Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for measuring discussions #32

Merged
merged 9 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
GH_TOKEN = " "
SEARCH_QUERY = "is:open is:issue"
REPOSITORY_URL = "https://github.com/github/fetch"
SEARCH_QUERY = "repo:owner/repo is:open is:issue"
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
disable=
redefined-argument-from-local,
too-many-arguments,
too-few-public-methods,
too-few-public-methods,
duplicate-code,
47 changes: 21 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

[![CodeQL](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/codeql-analysis.yml) [![Docker Image CI](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/docker-image.yml) [![Python package](https://github.com/github/issue-metrics/actions/workflows/python-package.yml/badge.svg)](https://github.com/github/issue-metrics/actions/workflows/python-package.yml)

This is a GitHub Action that searches for pull requests/issues in a repository and measures
This is a GitHub Action that searches for pull requests/issues/discussions in a repository and measures
the time to first response for each one. It then calculates the average time
to first response and writes the issues/pull requests with their time to first response and time to close
to a Markdown file. The issues/pull requests to search for can be filtered by using a search query.
to first response and writes the issues/pull requests/discussions with their metrics
to a Markdown file. The issues/pull requests/discussions to search for can be filtered by using a search query.

This action was developed by the GitHub OSPO for our own use and developed in a way that we could open source it that it might be useful to you as well! If you want to know more about how we use it, reach out in an issue in this repository.

Expand All @@ -18,7 +18,7 @@ If you need support using this project or have questions about it, please [open
## Use as a GitHub Action

1. Create a repository to host this GitHub Action or select an existing repository.
1. Create the env values from the sample workflow below (GH_TOKEN, REPOSITORY_URL, SEARCH_QUERY) with your information as repository secrets. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
1. Create the env values from the sample workflow below (GH_TOKEN, SEARCH_QUERY) with your information as repository secrets. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
Note: Your GitHub token will need to have read access to the repository in the organization that you want evaluated
1. Copy the below example workflow to your repository and put it in the `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/issue-metrics.yml`)

Expand All @@ -29,8 +29,7 @@ Below are the allowed configuration options:
| field | required | default | description |
|-----------------------|----------|---------|-------------|
| `GH_TOKEN` | true | | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. |
| `REPOSITORY_URL` | true | | The repository to scan for issues. |
| `SEARCH_QUERY` | true | | The query by which you can filter issues/prs |
| `SEARCH_QUERY` | true | | The query by which you can filter issues/prs which must contain a `repo:` entry or an `org:` entry. |

### Example workflows

Expand Down Expand Up @@ -77,8 +76,7 @@ jobs:
uses: github/issue-metrics@v1
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
REPOSITORY_URL: https://github.com/owner/repo
SEARCH_QUERY: 'is:issue created:${{ env.last_month }} -reason:"not planned"'
SEARCH_QUERY: 'repo:owner/repo is:issue created:${{ env.last_month }} -reason:"not planned"'

- name: Create issue
uses: peter-evans/create-issue-from-file@v4
Expand Down Expand Up @@ -108,8 +106,7 @@ jobs:
uses: github/issue-metrics@v1
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
REPOSITORY_URL: https://github.com/owner/repo
SEARCH_QUERY: 'is:issue created:2023-05-01..2023-05-31 -reason:"not planned"'
SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"'

- name: Create issue
uses: peter-evans/create-issue-from-file@v4
Expand All @@ -124,22 +121,22 @@ jobs:
This action can be configured to run metrics on pull requests and/or issues. It is also configurable by whether they were open or closed in the specified time window. Further query options are listed in [the search documentation](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests). Here are some search query examples:

Issues opened in May 2023:
- `is:issue created:2023-05-01..2023-05-31`
- `repo:owner/repo is:issue created:2023-05-01..2023-05-31`

Issues closed in May 2023 (may have been open in May or earlier):
- `is:issue closed:2023-05-01..2023-05-31`
- `repo:owner/repo is:issue closed:2023-05-01..2023-05-31`

Pull requests opened in May 2023:
- `is:pr created:2023-05-01..2023-05-31`
- `repo:owner/repo is:pr created:2023-05-01..2023-05-31`

Pull requests closed in May 2023 (may have been open in May or earlier):
- `is:pr closed:2023-05-01..2023-05-31`
- `repo:owner/repo is:pr closed:2023-05-01..2023-05-31`

Both issues and pull requests opened in May 2023:
- `created:2023-05-01..2023-05-31`
- `repo:owner/repo created:2023-05-01..2023-05-31`

Both issues and pull requests closed in May 2023 (may have been open in May or earlier):
- `closed:2023-05-01..2023-05-31`
- `repo:owner/repo closed:2023-05-01..2023-05-31`

OK, but what if I want both open or closed issues and pull requests? Due to limitations in issue search (no ability for OR logic), you will need to run the action twice, once for opened and once for closed. Here is an example workflow that does this:

Expand All @@ -161,8 +158,7 @@ jobs:
uses: github/issue-metrics:v1
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
REPOSITORY_URL: https://github.com/owner/repo
SEARCH_QUERY: 'created:2023-05-01..2023-05-31 -reason:"not planned"'
SEARCH_QUERY: 'repo:owner/repo created:2023-05-01..2023-05-31 -reason:"not planned"'

- name: Create issue for opened issues and prs
uses: peter-evans/create-issue-from-file@v4
Expand All @@ -175,8 +171,7 @@ jobs:
uses: github/issue-metrics:v1
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
REPOSITORY_URL: https://github.com/owner/repo
SEARCH_QUERY: 'closed:2023-05-01..2023-05-31 -reason:"not planned"'
SEARCH_QUERY: 'repo:owner/repo closed:2023-05-01..2023-05-31 -reason:"not planned"'

- name: Create issue for closed issues and prs
uses: peter-evans/create-issue-from-file@v4
Expand All @@ -195,23 +190,23 @@ jobs:
| --- | ---: |
| Average time to first response | 0:50:44.666667 |
| Average time to close | 6 days, 7:08:52 |
| Average time to answer | 1 day |
| Number of issues that remain open | 2 |
| Number of issues closed | 1 |
| Total number of issues created | 3 |

| Title | URL | Time to first response | Time to close
| --- | --- | ---: | ---: |
| Issue Title 1 | https://github.com/user/repo/issues/1 | 0:00:41 | 6 days, 7:08:52 |
| Issue Title 2 | https://github.com/user/repo/issues/2 | 0:05:26 | None |
| Issue Title 3 | https://github.com/user/repo/issues/3 | 2:26:07 | None |
| Title | URL | Time to first response | Time to close | Time to answer |
| --- | --- | ---: | ---: | ---: |
| Discussion Title 1 | https://github.com/user/repo/discussions/1 | 0:00:41 | 6 days, 7:08:52 | 1 day |
| Pull Request Title 2 | https://github.com/user/repo/pulls/2 | 0:05:26 | None | None |
| Issue Title 3 | https://github.com/user/repo/issues/3 | 2:26:07 | None | None |

```

## Local usage without Docker

1. Copy `.env-example` to `.env`
1. Fill out the `.env` file with a _token_ from a user that has access to the organization to scan (listed below). Tokens should have admin:org or read:org access.
1. Fill out the `.env` file with the _repository_url_ of the repository to scan
1. Fill out the `.env` file with the _search_query_ to filter issues by
1. `pip install -r requirements.txt`
1. Run `python3 ./issue_metrics.py`, which will output issue metrics data
Expand Down
35 changes: 35 additions & 0 deletions classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""A module containing classes for representing GitHub issues and their metrics.

Classes:
IssueWithMetrics: A class to represent a GitHub issue with metrics.

"""


class IssueWithMetrics:
"""A class to represent a GitHub issue with metrics.

Attributes:
title (str): The title of the issue.
html_url (str): The URL of the issue on GitHub.
time_to_first_response (timedelta, optional): The time it took to
get the first response to the issue.
time_to_close (timedelta, optional): The time it took to close the issue.
time_to_answer (timedelta, optional): The time it took to answer the
discussions in the issue.

"""

def __init__(
self,
title,
html_url,
time_to_first_response=None,
time_to_close=None,
time_to_answer=None,
):
self.title = title
self.html_url = html_url
self.time_to_first_response = time_to_first_response
self.time_to_close = time_to_close
self.time_to_answer = time_to_answer
72 changes: 72 additions & 0 deletions discussions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
This module provides functions for working with discussions in a GitHub repository.

Functions:
get_discussions(repo_url: str, token: str, search_query: str) -> List[Dict]:
Get a list of discussions in a GitHub repository that match the search query.

"""
import requests


def get_discussions(token: str, search_query: str):
"""Get a list of discussions in a GitHub repository that match the search query.

Args:
token (str): A personal access token for GitHub.
search_query (str): The search query to filter discussions by.

Returns:
list: A list of discussions in the repository that match the search query.

"""
# Construct the GraphQL query
query = """
query($query: String!) {
search(query: $query, type: DISCUSSION, first: 100) {
edges {
node {
... on Discussion {
title
url
createdAt
comments(first: 1) {
nodes {
createdAt
}
}
answerChosenAt
closedAt
}
}
}
}
}
"""

# Remove the type:discussions filter from the search query
search_query = search_query.replace("type:discussions ", "")
# Set the variables for the GraphQL query
variables = {"query": search_query}

# Send the GraphQL request
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(
"https://api.github.com/graphql",
json={"query": query, "variables": variables},
headers=headers,
timeout=60,
)

# Check for errors in the GraphQL response
if response.status_code != 200 or "errors" in response.json():
raise ValueError("GraphQL query failed")

data = response.json()["data"]

# Extract the discussions from the GraphQL response
discussions = []
for edge in data["search"]["edges"]:
discussions.append(edge["node"])

return discussions
Loading