Skip to content

Commit a122c9f

Browse files
authored
Merge pull request #158 from github/additional-metrics
feat: additional metrics for days since last release and PR
2 parents 93ea020 + 8f8ec06 commit a122c9f

File tree

4 files changed

+348
-125
lines changed

4 files changed

+348
-125
lines changed

.env-example

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
GH_APP_ID=' '
2-
GH_APP_INSTALLATION_ID=' '
3-
GH_APP_PRIVATE_KEY=' '
4-
GH_ENTERPRISE_URL=' '
5-
GH_TOKEN=' '
6-
INACTIVE_DAYS=365
7-
ORGANIZATION=' '
1+
ADDITIONAL_METRICS = ""
2+
GH_APP_ID = ""
3+
GH_APP_INSTALLATION_ID = ""
4+
GH_APP_PRIVATE_KEY = ""
5+
GH_ENTERPRISE_URL = ""
6+
GH_TOKEN = ""
7+
INACTIVE_DAYS = 365
8+
ORGANIZATION = ""

README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Stale Repos Action
32

43
[![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)
@@ -68,6 +67,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe
6867
| `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-*"` |
6968
| `EXEMPT_TOPICS` | false | | Comma separated list of topics to exempt from being flagged as stale |
7069
| `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 |
70+
| `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"` |
7171

7272
### Example workflow
7373

@@ -102,6 +102,7 @@ jobs:
102102
EXEMPT_TOPICS: "keep,template"
103103
INACTIVE_DAYS: 365
104104
ACTIVITY_METHOD: "pushed"
105+
ADDITIONAL_METRICS: "release,pr"
105106

106107
# 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.
107108
- name: Check for the stale report issue
@@ -129,9 +130,9 @@ jobs:
129130

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

132-
| Repository URL | Days Inactive | Last Push Date | Visibility |
133-
| --- | ---: | ---: | ---: |
134-
| https://github.com/github/.github | 5 | 2020-1-30 | private |
133+
| Repository URL | Days Inactive | Last Push Date | Visibility | Days Since Last Release | Days Since Last PR |
134+
| --- | ---: | ---: | ---: | ---: | ---: |
135+
| https://github.com/github/.github | 5 | 2020-1-30 | private | 10 | 7 |
135136
```
136137

137138
### Using JSON instead of Markdown
@@ -165,6 +166,7 @@ jobs:
165166
ORGANIZATION: ${{ secrets.ORGANIZATION }}
166167
EXEMPT_TOPICS: "keep,template"
167168
INACTIVE_DAYS: 365
169+
ADDITIONAL_METRICS: "release,pr"
168170

169171
- name: Print output of stale_repos tool
170172
run: echo "${{ steps.stale-repos.outputs.inactiveRepos }}"
@@ -212,6 +214,7 @@ jobs:
212214
GH_TOKEN: ${{ secrets.GH_TOKEN }}
213215
ORGANIZATION: ${{ matrix.org }}
214216
INACTIVE_DAYS: 365
217+
ADDITIONAL_METRICS: "release,pr"
215218
```
216219
217220
### Authenticating with a GitHub App and Installation
@@ -245,6 +248,7 @@ jobs:
245248
EXEMPT_TOPICS: "keep,template"
246249
INACTIVE_DAYS: 365
247250
ACTIVITY_METHOD: "pushed"
251+
ADDITIONAL_METRICS: "release,pr"
248252
```
249253

250254
## Local usage without Docker

stale_repos.py

+132-26
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@ def main(): # pragma: no cover
4747
"ORGANIZATION environment variable not set, searching all repos owned by token owner"
4848
)
4949

50+
# Fetch additional metrics configuration
51+
additional_metrics = os.getenv("ADDITIONAL_METRICS", "").split(",")
52+
5053
# Iterate over repos in the org, acquire inactive days,
5154
# and print out the repo url and days inactive if it's over the threshold (inactive_days)
5255
inactive_repos = get_inactive_repos(
53-
github_connection, inactive_days_threshold, organization
56+
github_connection, inactive_days_threshold, organization, additional_metrics
5457
)
5558

5659
if inactive_repos:
5760
output_to_json(inactive_repos)
58-
write_to_markdown(inactive_repos, inactive_days_threshold)
61+
write_to_markdown(inactive_repos, inactive_days_threshold, additional_metrics)
5962
else:
6063
print("No stale repos found")
6164

@@ -91,14 +94,17 @@ def is_repo_exempt(repo, exempt_repos, exempt_topics):
9194
return False
9295

9396

94-
def get_inactive_repos(github_connection, inactive_days_threshold, organization):
97+
def get_inactive_repos(
98+
github_connection, inactive_days_threshold, organization, additional_metrics=None
99+
):
95100
"""Return and print out the repo url and days inactive if it's over
96101
the threshold (inactive_days).
97102
98103
Args:
99104
github_connection: The GitHub connection object.
100105
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
101106
organization: The name of the organization to retrieve repositories from.
107+
additional_metrics: A list of additional metrics to include in the report.
102108
103109
Returns:
104110
A list of tuples containing the repo, days inactive, the date of the last push and
@@ -137,17 +143,49 @@ def get_inactive_repos(github_connection, inactive_days_threshold, organization)
137143
days_inactive = (datetime.now(timezone.utc) - active_date).days
138144
visibility = "private" if repo.private else "public"
139145
if days_inactive > int(inactive_days_threshold):
140-
inactive_repos.append(
141-
(repo.html_url, days_inactive, active_date_disp, visibility)
146+
repo_data = set_repo_data(
147+
repo, days_inactive, active_date_disp, visibility, additional_metrics
142148
)
143-
print(f"{repo.html_url}: {days_inactive} days inactive") # type: ignore
149+
inactive_repos.append(repo_data)
144150
if organization:
145151
print(f"Found {len(inactive_repos)} stale repos in {organization}")
146152
else:
147153
print(f"Found {len(inactive_repos)} stale repos")
148154
return inactive_repos
149155

150156

157+
def get_days_since_last_release(repo):
158+
"""Get the number of days since the last release of the repository.
159+
160+
Args:
161+
repo: A Github repository object.
162+
163+
Returns:
164+
The number of days since the last release.
165+
"""
166+
try:
167+
last_release = next(repo.releases())
168+
return (datetime.now(timezone.utc) - last_release.created_at).days
169+
except StopIteration:
170+
return None
171+
172+
173+
def get_days_since_last_pr(repo):
174+
"""Get the number of days since the last pull request was made in the repository.
175+
176+
Args:
177+
repo: A Github repository object.
178+
179+
Returns:
180+
The number of days since the last pull request was made.
181+
"""
182+
try:
183+
last_pr = next(repo.pull_requests(state="all"))
184+
return (datetime.now(timezone.utc) - last_pr.created_at).days
185+
except StopIteration:
186+
return None
187+
188+
151189
def get_active_date(repo):
152190
"""Get the last activity date of the repository.
153191
@@ -180,41 +218,68 @@ def get_active_date(repo):
180218
return active_date
181219

182220

183-
def write_to_markdown(inactive_repos, inactive_days_threshold, file=None):
221+
def write_to_markdown(
222+
inactive_repos, inactive_days_threshold, additional_metrics=None, file=None
223+
):
184224
"""Write the list of inactive repos to a markdown file.
185225
186226
Args:
187-
inactive_repos: A list of tuples containing the repo, days inactive,
188-
the date of the last push, and repository visibility (public/private).
227+
inactive_repos: A list of dictionaries containing the repo, days inactive,
228+
the date of the last push, repository visibility (public/private),
229+
days since the last release, and days since the last pr
189230
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
231+
additional_metrics: A list of additional metrics to include in the report.
190232
file: A file object to write to. If None, a new file will be created.
191233
192234
"""
193-
inactive_repos.sort(key=lambda x: x[1], reverse=True)
235+
inactive_repos = sorted(
236+
inactive_repos, key=lambda x: x["days_inactive"], reverse=True
237+
)
194238
with file or open("stale_repos.md", "w", encoding="utf-8") as markdown_file:
195239
markdown_file.write("# Inactive Repositories\n\n")
196240
markdown_file.write(
197241
f"The following repos have not had a push event for more than "
198242
f"{inactive_days_threshold} days:\n\n"
199243
)
200244
markdown_file.write(
201-
"| Repository URL | Days Inactive | Last Push Date | Visibility |\n"
245+
"| Repository URL | Days Inactive | Last Push Date | Visibility |"
202246
)
203-
markdown_file.write("| --- | --- | --- | ---: |\n")
204-
for repo_url, days_inactive, last_push_date, visibility in inactive_repos:
247+
# Include additional metrics columns if configured
248+
if additional_metrics:
249+
if "release" in additional_metrics:
250+
markdown_file.write(" Days Since Last Release |")
251+
if "pr" in additional_metrics:
252+
markdown_file.write(" Days Since Last PR |")
253+
markdown_file.write("\n| --- | --- | --- | ---: |")
254+
if additional_metrics and (
255+
"release" in additional_metrics or "pr" in additional_metrics
256+
):
257+
markdown_file.write(" ---: |")
258+
markdown_file.write("\n")
259+
for repo_data in inactive_repos:
205260
markdown_file.write(
206-
f"| {repo_url} | {days_inactive} | {last_push_date} | {visibility} |\n"
261+
f"| {repo_data['url']} \
262+
| {repo_data['days_inactive']} \
263+
| {repo_data['last_push_date']} \
264+
| {repo_data['visibility']} |"
207265
)
266+
if additional_metrics:
267+
if "release" in additional_metrics:
268+
markdown_file.write(f" {repo_data['days_since_last_release']} |")
269+
if "pr" in additional_metrics:
270+
markdown_file.write(f" {repo_data['days_since_last_pr']} |")
271+
markdown_file.write("\n")
208272
print("Wrote stale repos to stale_repos.md")
209273

210274

211275
def output_to_json(inactive_repos, file=None):
212276
"""Convert the list of inactive repos to a json string.
213277
214278
Args:
215-
inactive_repos: A list of tuples containing the repo,
216-
days inactive, the date of the last push, and
217-
visiblity of the repository (public/private).
279+
inactive_repos: A list of dictionaries containing the repo,
280+
days inactive, the date of the last push,
281+
visiblity of the repository (public/private),
282+
days since the last release, and days since the last pr.
218283
219284
Returns:
220285
JSON formatted string of the list of inactive repos.
@@ -226,18 +291,23 @@ def output_to_json(inactive_repos, file=None):
226291
# "url": "https://github.com/owner/repo",
227292
# "daysInactive": 366,
228293
# "lastPushDate": "2020-01-01"
294+
# "daysSinceLastRelease": "5"
295+
# "daysSinceLastPR": "10"
229296
# }
230297
# ]
231298
inactive_repos_json = []
232-
for repo_url, days_inactive, last_push_date, visibility in inactive_repos:
233-
inactive_repos_json.append(
234-
{
235-
"url": repo_url,
236-
"daysInactive": days_inactive,
237-
"lastPushDate": last_push_date,
238-
"visibility": visibility,
239-
}
240-
)
299+
for repo_data in inactive_repos:
300+
repo_json = {
301+
"url": repo_data["url"],
302+
"daysInactive": repo_data["days_inactive"],
303+
"lastPushDate": repo_data["last_push_date"],
304+
"visibility": repo_data["visibility"],
305+
}
306+
if "release" in repo_data:
307+
repo_json["daysSinceLastRelease"] = repo_data["days_since_last_release"]
308+
if "pr" in repo_data:
309+
repo_json["daysSinceLastPR"] = repo_data["days_since_last_pr"]
310+
inactive_repos_json.append(repo_json)
241311
inactive_repos_json = json.dumps(inactive_repos_json)
242312

243313
# add output to github action output
@@ -298,5 +368,41 @@ def auth_to_github():
298368
return github_connection # type: ignore
299369

300370

371+
def set_repo_data(
372+
repo, days_inactive, active_date_disp, visibility, additional_metrics
373+
):
374+
"""
375+
Constructs a dictionary with repository data
376+
including optional metrics based on additional metrics specified.
377+
378+
Args:
379+
repo: The repository object.
380+
days_inactive: Number of days the repository has been inactive.
381+
active_date_disp: The display string of the last active date.
382+
visibility: The visibility status of the repository (e.g., private or public).
383+
additional_metrics: A list of strings indicating which additional metrics to include.
384+
385+
Returns:
386+
A dictionary with the repository data.
387+
"""
388+
repo_data = {
389+
"url": repo.html_url,
390+
"days_inactive": days_inactive,
391+
"last_push_date": active_date_disp,
392+
"visibility": visibility,
393+
}
394+
# Fetch and include additional metrics if configured
395+
repo_data["days_since_last_release"] = None
396+
repo_data["days_since_last_pr"] = None
397+
if additional_metrics:
398+
if "release" in additional_metrics:
399+
repo_data["days_since_last_release"] = get_days_since_last_release(repo)
400+
if "pr" in additional_metrics:
401+
repo_data["days_since_last_pr"] = get_days_since_last_pr(repo)
402+
403+
print(f"{repo.html_url}: {days_inactive} days inactive") # type: ignore
404+
return repo_data
405+
406+
301407
if __name__ == "__main__":
302408
main()

0 commit comments

Comments
 (0)