Skip to content

Commit

Permalink
feat: additional metrics (last release and last pr)
Browse files Browse the repository at this point in the history
Signed-off-by: Zack Koppert <[email protected]>
  • Loading branch information
zkoppert committed Jun 8, 2024
1 parent a7f5e74 commit 8f8ec06
Show file tree
Hide file tree
Showing 4 changed files with 348 additions and 125 deletions.
15 changes: 8 additions & 7 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
GH_APP_ID=' '
GH_APP_INSTALLATION_ID=' '
GH_APP_PRIVATE_KEY=' '
GH_ENTERPRISE_URL=' '
GH_TOKEN=' '
INACTIVE_DAYS=365
ORGANIZATION=' '
ADDITIONAL_METRICS = ""
GH_APP_ID = ""
GH_APP_INSTALLATION_ID = ""
GH_APP_PRIVATE_KEY = ""
GH_ENTERPRISE_URL = ""
GH_TOKEN = ""
INACTIVE_DAYS = 365
ORGANIZATION = ""
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Stale Repos Action

[![Lint Code Base](https://github.com/github/stale-repos/actions/workflows/linter.yaml/badge.svg)](https://github.com/github/stale-repos/actions/workflows/linter.yaml)
Expand Down Expand Up @@ -68,6 +67,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe
| `EXEMPT_REPOS` | false | | Comma separated list of repositories to exempt from being flagged as stale. Supports Unix shell-style wildcards. ie. `EXEMPT_REPOS = "stale-repos,test-repo,conf-*"` |
| `EXEMPT_TOPICS` | false | | Comma separated list of topics to exempt from being flagged as stale |
| `ORGANIZATION` | false | | The organization to scan for stale repositories. If no organization is provided, this tool will search through repositories owned by the GH_TOKEN owner |
| `ADDITIONAL_METRICS` | false | | Configure additional metrics like days since last release or days since last pull request. This allows for more detailed reporting on repository activity. To include both metrics, set `ADDITIONAL_METRICS: "release,pr"` |

### Example workflow

Expand Down Expand Up @@ -102,6 +102,7 @@ jobs:
EXEMPT_TOPICS: "keep,template"
INACTIVE_DAYS: 365
ACTIVITY_METHOD: "pushed"
ADDITIONAL_METRICS: "release,pr"

# This next step updates an existing issue. If you want a new issue every time, remove this step and remove the `issue-number: ${{ env.issue_number }}` line below.
- name: Check for the stale report issue
Expand Down Expand Up @@ -129,9 +130,9 @@ jobs:

The following repos have not had a push event for more than 3 days:

| Repository URL | Days Inactive | Last Push Date | Visibility |
| --- | ---: | ---: | ---: |
| https://github.com/github/.github | 5 | 2020-1-30 | private |
| Repository URL | Days Inactive | Last Push Date | Visibility | Days Since Last Release | Days Since Last PR |
| --- | ---: | ---: | ---: | ---: | ---: |
| https://github.com/github/.github | 5 | 2020-1-30 | private | 10 | 7 |
```

### Using JSON instead of Markdown
Expand Down Expand Up @@ -165,6 +166,7 @@ jobs:
ORGANIZATION: ${{ secrets.ORGANIZATION }}
EXEMPT_TOPICS: "keep,template"
INACTIVE_DAYS: 365
ADDITIONAL_METRICS: "release,pr"

- name: Print output of stale_repos tool
run: echo "${{ steps.stale-repos.outputs.inactiveRepos }}"
Expand Down Expand Up @@ -212,6 +214,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
ORGANIZATION: ${{ matrix.org }}
INACTIVE_DAYS: 365
ADDITIONAL_METRICS: "release,pr"
```
### Authenticating with a GitHub App and Installation
Expand Down Expand Up @@ -245,6 +248,7 @@ jobs:
EXEMPT_TOPICS: "keep,template"
INACTIVE_DAYS: 365
ACTIVITY_METHOD: "pushed"
ADDITIONAL_METRICS: "release,pr"
```

## Local usage without Docker
Expand Down
158 changes: 132 additions & 26 deletions stale_repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,18 @@ def main(): # pragma: no cover
"ORGANIZATION environment variable not set, searching all repos owned by token owner"
)

# Fetch additional metrics configuration
additional_metrics = os.getenv("ADDITIONAL_METRICS", "").split(",")

# 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)
inactive_repos = get_inactive_repos(
github_connection, inactive_days_threshold, organization
github_connection, inactive_days_threshold, organization, additional_metrics
)

if inactive_repos:
output_to_json(inactive_repos)
write_to_markdown(inactive_repos, inactive_days_threshold)
write_to_markdown(inactive_repos, inactive_days_threshold, additional_metrics)
else:
print("No stale repos found")

Expand Down Expand Up @@ -91,14 +94,17 @@ def is_repo_exempt(repo, exempt_repos, exempt_topics):
return False


def get_inactive_repos(github_connection, inactive_days_threshold, organization):
def get_inactive_repos(
github_connection, inactive_days_threshold, organization, additional_metrics=None
):
"""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.
additional_metrics: A list of additional metrics to include in the report.
Returns:
A list of tuples containing the repo, days inactive, the date of the last push and
Expand Down Expand Up @@ -137,17 +143,49 @@ def get_inactive_repos(github_connection, inactive_days_threshold, organization)
days_inactive = (datetime.now(timezone.utc) - active_date).days
visibility = "private" if repo.private else "public"
if days_inactive > int(inactive_days_threshold):
inactive_repos.append(
(repo.html_url, days_inactive, active_date_disp, visibility)
repo_data = set_repo_data(
repo, days_inactive, active_date_disp, visibility, additional_metrics
)
print(f"{repo.html_url}: {days_inactive} days inactive") # type: ignore
inactive_repos.append(repo_data)
if organization:
print(f"Found {len(inactive_repos)} stale repos in {organization}")
else:
print(f"Found {len(inactive_repos)} stale repos")
return inactive_repos


def get_days_since_last_release(repo):
"""Get the number of days since the last release of the repository.
Args:
repo: A Github repository object.
Returns:
The number of days since the last release.
"""
try:
last_release = next(repo.releases())
return (datetime.now(timezone.utc) - last_release.created_at).days
except StopIteration:
return None


def get_days_since_last_pr(repo):
"""Get the number of days since the last pull request was made in the repository.
Args:
repo: A Github repository object.
Returns:
The number of days since the last pull request was made.
"""
try:
last_pr = next(repo.pull_requests(state="all"))
return (datetime.now(timezone.utc) - last_pr.created_at).days
except StopIteration:
return None


def get_active_date(repo):
"""Get the last activity date of the repository.
Expand Down Expand Up @@ -180,41 +218,68 @@ def get_active_date(repo):
return active_date


def write_to_markdown(inactive_repos, inactive_days_threshold, file=None):
def write_to_markdown(
inactive_repos, inactive_days_threshold, additional_metrics=None, file=None
):
"""Write the list of inactive repos to a markdown file.
Args:
inactive_repos: A list of tuples containing the repo, days inactive,
the date of the last push, and repository visibility (public/private).
inactive_repos: A list of dictionaries containing the repo, days inactive,
the date of the last push, repository visibility (public/private),
days since the last release, and days since the last pr
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
additional_metrics: A list of additional metrics to include in the report.
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)
inactive_repos = sorted(
inactive_repos, key=lambda x: x["days_inactive"], reverse=True
)
with file or open("stale_repos.md", "w", encoding="utf-8") as markdown_file:
markdown_file.write("# Inactive Repositories\n\n")
markdown_file.write(
f"The following repos have not had a push event for more than "
f"{inactive_days_threshold} days:\n\n"
)
markdown_file.write(
"| Repository URL | Days Inactive | Last Push Date | Visibility |\n"
"| Repository URL | Days Inactive | Last Push Date | Visibility |"
)
markdown_file.write("| --- | --- | --- | ---: |\n")
for repo_url, days_inactive, last_push_date, visibility in inactive_repos:
# Include additional metrics columns if configured
if additional_metrics:
if "release" in additional_metrics:
markdown_file.write(" Days Since Last Release |")
if "pr" in additional_metrics:
markdown_file.write(" Days Since Last PR |")
markdown_file.write("\n| --- | --- | --- | ---: |")
if additional_metrics and (
"release" in additional_metrics or "pr" in additional_metrics
):
markdown_file.write(" ---: |")
markdown_file.write("\n")
for repo_data in inactive_repos:
markdown_file.write(
f"| {repo_url} | {days_inactive} | {last_push_date} | {visibility} |\n"
f"| {repo_data['url']} \
| {repo_data['days_inactive']} \
| {repo_data['last_push_date']} \
| {repo_data['visibility']} |"
)
if additional_metrics:
if "release" in additional_metrics:
markdown_file.write(f" {repo_data['days_since_last_release']} |")
if "pr" in additional_metrics:
markdown_file.write(f" {repo_data['days_since_last_pr']} |")
markdown_file.write("\n")
print("Wrote stale repos to stale_repos.md")


def output_to_json(inactive_repos, file=None):
"""Convert the list of inactive repos to a json string.
Args:
inactive_repos: A list of tuples containing the repo,
days inactive, the date of the last push, and
visiblity of the repository (public/private).
inactive_repos: A list of dictionaries containing the repo,
days inactive, the date of the last push,
visiblity of the repository (public/private),
days since the last release, and days since the last pr.
Returns:
JSON formatted string of the list of inactive repos.
Expand All @@ -226,18 +291,23 @@ def output_to_json(inactive_repos, file=None):
# "url": "https://github.com/owner/repo",
# "daysInactive": 366,
# "lastPushDate": "2020-01-01"
# "daysSinceLastRelease": "5"
# "daysSinceLastPR": "10"
# }
# ]
inactive_repos_json = []
for repo_url, days_inactive, last_push_date, visibility in inactive_repos:
inactive_repos_json.append(
{
"url": repo_url,
"daysInactive": days_inactive,
"lastPushDate": last_push_date,
"visibility": visibility,
}
)
for repo_data in inactive_repos:
repo_json = {
"url": repo_data["url"],
"daysInactive": repo_data["days_inactive"],
"lastPushDate": repo_data["last_push_date"],
"visibility": repo_data["visibility"],
}
if "release" in repo_data:
repo_json["daysSinceLastRelease"] = repo_data["days_since_last_release"]
if "pr" in repo_data:
repo_json["daysSinceLastPR"] = repo_data["days_since_last_pr"]
inactive_repos_json.append(repo_json)
inactive_repos_json = json.dumps(inactive_repos_json)

# add output to github action output
Expand Down Expand Up @@ -298,5 +368,41 @@ def auth_to_github():
return github_connection # type: ignore


def set_repo_data(
repo, days_inactive, active_date_disp, visibility, additional_metrics
):
"""
Constructs a dictionary with repository data
including optional metrics based on additional metrics specified.
Args:
repo: The repository object.
days_inactive: Number of days the repository has been inactive.
active_date_disp: The display string of the last active date.
visibility: The visibility status of the repository (e.g., private or public).
additional_metrics: A list of strings indicating which additional metrics to include.
Returns:
A dictionary with the repository data.
"""
repo_data = {
"url": repo.html_url,
"days_inactive": days_inactive,
"last_push_date": active_date_disp,
"visibility": visibility,
}
# Fetch and include additional metrics if configured
repo_data["days_since_last_release"] = None
repo_data["days_since_last_pr"] = None
if additional_metrics:
if "release" in additional_metrics:
repo_data["days_since_last_release"] = get_days_since_last_release(repo)
if "pr" in additional_metrics:
repo_data["days_since_last_pr"] = get_days_since_last_pr(repo)

print(f"{repo.html_url}: {days_inactive} days inactive") # type: ignore
return repo_data


if __name__ == "__main__":
main()
Loading

0 comments on commit 8f8ec06

Please sign in to comment.