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

feat: additional metrics for days since last release and PR #158

Merged
merged 1 commit into from
Jun 10, 2024
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
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 = ""
zkoppert marked this conversation as resolved.
Show resolved Hide resolved
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"` |
jmeridth marked this conversation as resolved.
Show resolved Hide resolved

### 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
Comment on lines +157 to +186
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core functionality change



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