Skip to content

Commit 82e379f

Browse files
authored
Merge pull request #362 from github/copilot/fix-226
feat: Add WORKFLOW_SUMMARY_ENABLED flag to automatically output stale repo report to GitHub Actions workflow summary
2 parents 0e098ab + 2e5ec32 commit 82e379f

File tree

9 files changed

+411
-150
lines changed

9 files changed

+411
-150
lines changed

.github/linters/.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[settings]
22
profile = black
33
known_third_party = github3,dateutil,dotenv
4-
known_first_party = auth
4+
known_first_party = auth,markdown

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,8 @@ cython_debug/
143143

144144
# IDEA
145145
.idea/**
146+
147+
# Node.js
148+
node_modules/
149+
package-lock.json
150+
package.json

README.md

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,17 @@ This action can be configured to authenticate with GitHub App Installation or Pe
6060

6161
#### Other Configuration Options
6262

63-
| field | required | default | description |
64-
| -------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65-
| `ACTIVITY_METHOD` | false | `"pushed"` | How to get the last active date of the repository. Defaults to `pushed`, which is the last time any branch had a push. Can also be set to `default_branch_updated` to instead measure from the latest commit on the default branch (good for filtering out dependabot ) |
66-
| `GH_ENTERPRISE_URL` | false | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com |
67-
| `INACTIVE_DAYS` | true | | The number of days used to determine if repository is stale, based on `push` events |
68-
| `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-*"` |
69-
| `EXEMPT_TOPICS` | false | | Comma separated list of topics to exempt from being flagged as stale |
70-
| `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 |
71-
| `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"` |
72-
| `SKIP_EMPTY_REPORTS` | false | `true` | Skips report creation when no stale repositories are identified. Setting this input to `false` means reports are always created, even when they contain no results. |
63+
| field | required | default | description |
64+
| -------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65+
| `ACTIVITY_METHOD` | false | `"pushed"` | How to get the last active date of the repository. Defaults to `pushed`, which is the last time any branch had a push. Can also be set to `default_branch_updated` to instead measure from the latest commit on the default branch (good for filtering out dependabot ) |
66+
| `GH_ENTERPRISE_URL` | false | `""` | URL of GitHub Enterprise instance to use for auth instead of github.com |
67+
| `INACTIVE_DAYS` | true | | The number of days used to determine if repository is stale, based on `push` events |
68+
| `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-*"` |
69+
| `EXEMPT_TOPICS` | false | | Comma separated list of topics to exempt from being flagged as stale |
70+
| `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 |
71+
| `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"` |
72+
| `SKIP_EMPTY_REPORTS` | false | `true` | Skips report creation when no stale repositories are identified. Setting this input to `false` means reports are always created, even when they contain no results. |
73+
| `WORKFLOW_SUMMARY_ENABLED` | false | `false` | When set to `true`, automatically adds the stale repository report to the GitHub Actions workflow summary. This eliminates the need to manually add a step to display the Markdown content in the workflow summary. |
7374

7475
### Example workflow
7576

@@ -124,6 +125,40 @@ jobs:
124125
token: ${{ secrets.GITHUB_TOKEN }}
125126
```
126127
128+
### Using Workflow Summary
129+
130+
You can automatically include the stale repository report in your GitHub Actions workflow summary by setting `WORKFLOW_SUMMARY_ENABLED: true`. This eliminates the need for additional steps to display the results.
131+
132+
```yaml
133+
name: stale repo identifier
134+
135+
on:
136+
workflow_dispatch:
137+
schedule:
138+
- cron: "3 2 1 * *"
139+
140+
permissions:
141+
contents: read
142+
143+
jobs:
144+
build:
145+
name: stale repo identifier
146+
runs-on: ubuntu-latest
147+
148+
steps:
149+
- name: Run stale_repos tool
150+
uses: github/stale-repos@v3
151+
env:
152+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
153+
ORGANIZATION: ${{ secrets.ORGANIZATION }}
154+
EXEMPT_TOPICS: "keep,template"
155+
INACTIVE_DAYS: 365
156+
ADDITIONAL_METRICS: "release,pr"
157+
WORKFLOW_SUMMARY_ENABLED: true
158+
```
159+
160+
When `WORKFLOW_SUMMARY_ENABLED` is set to `true`, the stale repository report will be automatically added to the GitHub Actions workflow summary, making it easy to see the results directly in the workflow run page.
161+
127162
### Example stale_repos.md output
128163

129164
```markdown

env.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class EnvVars:
2828
ghe (str): The GitHub Enterprise URL to use for authentication
2929
skip_empty_reports (bool): If true, Skips report creation when no stale
3030
repositories are identified
31+
workflow_summary_enabled (bool): If true, adds the markdown report to GitHub
32+
Actions workflow summary
3133
"""
3234

3335
def __init__(
@@ -39,6 +41,7 @@ def __init__(
3941
gh_token: str | None,
4042
ghe: str | None,
4143
skip_empty_reports: bool,
44+
workflow_summary_enabled: bool,
4245
):
4346
self.gh_app_id = gh_app_id
4447
self.gh_app_installation_id = gh_app_installation_id
@@ -47,6 +50,7 @@ def __init__(
4750
self.gh_token = gh_token
4851
self.ghe = ghe
4952
self.skip_empty_reports = skip_empty_reports
53+
self.workflow_summary_enabled = workflow_summary_enabled
5054

5155
def __repr__(self):
5256
return (
@@ -58,6 +62,7 @@ def __repr__(self):
5862
f"{self.gh_token},"
5963
f"{self.ghe},"
6064
f"{self.skip_empty_reports},"
65+
f"{self.workflow_summary_enabled},"
6166
)
6267

6368

@@ -120,6 +125,7 @@ def get_env_vars(
120125
ghe = os.getenv("GH_ENTERPRISE_URL")
121126
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")
122127
skip_empty_reports = get_bool_env_var("SKIP_EMPTY_REPORTS", True)
128+
workflow_summary_enabled = get_bool_env_var("WORKFLOW_SUMMARY_ENABLED")
123129

124130
if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
125131
raise ValueError(
@@ -142,4 +148,5 @@ def get_env_vars(
142148
gh_token,
143149
ghe,
144150
skip_empty_reports,
151+
workflow_summary_enabled,
145152
)

markdown.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Markdown utilities for stale repository reporting."""
2+
3+
import os
4+
5+
6+
def write_to_markdown(
7+
inactive_repos,
8+
inactive_days_threshold,
9+
additional_metrics=None,
10+
workflow_summary_enabled=False,
11+
file=None,
12+
):
13+
"""Write the list of inactive repos to a markdown file.
14+
15+
Args:
16+
inactive_repos: A list of dictionaries containing the repo, days inactive,
17+
the date of the last push, repository visibility (public/private),
18+
days since the last release, and days since the last pr
19+
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
20+
additional_metrics: A list of additional metrics to include in the report.
21+
workflow_summary_enabled: If True, adds the report to GitHub Actions workflow summary.
22+
file: A file object to write to. If None, a new file will be created.
23+
24+
"""
25+
inactive_repos = sorted(
26+
inactive_repos, key=lambda x: x["days_inactive"], reverse=True
27+
)
28+
29+
# Generate markdown content
30+
content = generate_markdown_content(
31+
inactive_repos, inactive_days_threshold, additional_metrics
32+
)
33+
34+
# Write to file
35+
with file or open("stale_repos.md", "w", encoding="utf-8") as markdown_file:
36+
markdown_file.write(content)
37+
print("Wrote stale repos to stale_repos.md")
38+
39+
# Write to GitHub step summary if enabled
40+
if workflow_summary_enabled and os.environ.get("GITHUB_STEP_SUMMARY"):
41+
with open(
42+
os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8"
43+
) as summary_file:
44+
summary_file.write(content)
45+
print("Added stale repos to workflow summary")
46+
47+
48+
def generate_markdown_content(
49+
inactive_repos, inactive_days_threshold, additional_metrics=None
50+
):
51+
"""Generate markdown content for the inactive repos report.
52+
53+
Args:
54+
inactive_repos: A list of dictionaries containing the repo, days inactive,
55+
the date of the last push, repository visibility (public/private),
56+
days since the last release, and days since the last pr
57+
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
58+
additional_metrics: A list of additional metrics to include in the report.
59+
60+
Returns:
61+
str: The generated markdown content.
62+
"""
63+
content = "# Inactive Repositories\n\n"
64+
content += (
65+
f"The following repos have not had a push event for more than "
66+
f"{inactive_days_threshold} days:\n\n"
67+
)
68+
content += "| Repository URL | Days Inactive | Last Push Date | Visibility |"
69+
70+
# Include additional metrics columns if configured
71+
if additional_metrics:
72+
if "release" in additional_metrics:
73+
content += " Days Since Last Release |"
74+
if "pr" in additional_metrics:
75+
content += " Days Since Last PR |"
76+
content += "\n| --- | --- | --- | --- |"
77+
if additional_metrics:
78+
if "release" in additional_metrics:
79+
content += " --- |"
80+
if "pr" in additional_metrics:
81+
content += " --- |"
82+
content += "\n"
83+
84+
for repo_data in inactive_repos:
85+
content += (
86+
f"| {repo_data['url']} "
87+
f"| {repo_data['days_inactive']} "
88+
f"| {repo_data['last_push_date']} "
89+
f"| {repo_data['visibility']} |"
90+
)
91+
if additional_metrics:
92+
if "release" in additional_metrics:
93+
content += f" {repo_data['days_since_last_release']} |"
94+
if "pr" in additional_metrics:
95+
content += f" {repo_data['days_since_last_pr']} |"
96+
content += "\n"
97+
98+
return content

stale_repos.py

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from env import get_env_vars
1212

1313
import auth
14+
from markdown import write_to_markdown
1415

1516

1617
def main(): # pragma: no cover
@@ -38,6 +39,7 @@ def main(): # pragma: no cover
3839
ghe = env_vars.ghe
3940
gh_app_enterprise_only = env_vars.gh_app_enterprise_only
4041
skip_empty_reports = env_vars.skip_empty_reports
42+
workflow_summary_enabled = env_vars.workflow_summary_enabled
4143

4244
# Auth to GitHub.com or GHE
4345
github_connection = auth.auth_to_github(
@@ -72,7 +74,12 @@ def main(): # pragma: no cover
7274

7375
if inactive_repos or not skip_empty_reports:
7476
output_to_json(inactive_repos)
75-
write_to_markdown(inactive_repos, inactive_days_threshold, additional_metrics)
77+
write_to_markdown(
78+
inactive_repos,
79+
inactive_days_threshold,
80+
additional_metrics,
81+
workflow_summary_enabled,
82+
)
7683
else:
7784
print("Reporting skipped; no stale repos found.")
7885

@@ -235,61 +242,6 @@ def get_active_date(repo):
235242
return active_date
236243

237244

238-
def write_to_markdown(
239-
inactive_repos, inactive_days_threshold, additional_metrics=None, file=None
240-
):
241-
"""Write the list of inactive repos to a markdown file.
242-
243-
Args:
244-
inactive_repos: A list of dictionaries containing the repo, days inactive,
245-
the date of the last push, repository visibility (public/private),
246-
days since the last release, and days since the last pr
247-
inactive_days_threshold: The threshold (in days) for considering a repo as inactive.
248-
additional_metrics: A list of additional metrics to include in the report.
249-
file: A file object to write to. If None, a new file will be created.
250-
251-
"""
252-
inactive_repos = sorted(
253-
inactive_repos, key=lambda x: x["days_inactive"], reverse=True
254-
)
255-
with file or open("stale_repos.md", "w", encoding="utf-8") as markdown_file:
256-
markdown_file.write("# Inactive Repositories\n\n")
257-
markdown_file.write(
258-
f"The following repos have not had a push event for more than "
259-
f"{inactive_days_threshold} days:\n\n"
260-
)
261-
markdown_file.write(
262-
"| Repository URL | Days Inactive | Last Push Date | Visibility |"
263-
)
264-
# Include additional metrics columns if configured
265-
if additional_metrics:
266-
if "release" in additional_metrics:
267-
markdown_file.write(" Days Since Last Release |")
268-
if "pr" in additional_metrics:
269-
markdown_file.write(" Days Since Last PR |")
270-
markdown_file.write("\n| --- | --- | --- | --- |")
271-
if additional_metrics:
272-
if "release" in additional_metrics:
273-
markdown_file.write(" --- |")
274-
if "pr" in additional_metrics:
275-
markdown_file.write(" --- |")
276-
markdown_file.write("\n")
277-
for repo_data in inactive_repos:
278-
markdown_file.write(
279-
f"| {repo_data['url']} \
280-
| {repo_data['days_inactive']} \
281-
| {repo_data['last_push_date']} \
282-
| {repo_data['visibility']} |"
283-
)
284-
if additional_metrics:
285-
if "release" in additional_metrics:
286-
markdown_file.write(f" {repo_data['days_since_last_release']} |")
287-
if "pr" in additional_metrics:
288-
markdown_file.write(f" {repo_data['days_since_last_pr']} |")
289-
markdown_file.write("\n")
290-
print("Wrote stale repos to stale_repos.md")
291-
292-
293245
def output_to_json(inactive_repos, file=None):
294246
"""Convert the list of inactive repos to a json string.
295247

test_env.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def test_get_env_vars_with_github_app(self):
5454
gh_token="",
5555
ghe="",
5656
skip_empty_reports=True,
57+
workflow_summary_enabled=False,
5758
)
5859
result = get_env_vars(True)
5960
self.assertEqual(str(result), str(expected_result))
@@ -79,6 +80,7 @@ def test_get_env_vars_with_token(self):
7980
gh_token=TOKEN,
8081
ghe="",
8182
skip_empty_reports=True,
83+
workflow_summary_enabled=False,
8284
)
8385
result = get_env_vars(True)
8486
self.assertEqual(str(result), str(expected_result))
@@ -119,6 +121,7 @@ def test_get_env_vars_optional_values(self):
119121
gh_token=TOKEN,
120122
ghe="",
121123
skip_empty_reports=False,
124+
workflow_summary_enabled=False,
122125
)
123126
result = get_env_vars(True)
124127
self.assertEqual(str(result), str(expected_result))
@@ -143,6 +146,7 @@ def test_get_env_vars_optionals_are_defaulted(self):
143146
gh_token="TOKEN",
144147
ghe=None,
145148
skip_empty_reports=True,
149+
workflow_summary_enabled=False,
146150
)
147151
result = get_env_vars(True)
148152
self.assertEqual(str(result), str(expected_result))
@@ -168,6 +172,29 @@ def test_get_env_vars_auth_with_github_app_installation_missing_inputs(self):
168172
"GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set",
169173
)
170174

175+
@patch.dict(
176+
os.environ,
177+
{
178+
"GH_TOKEN": "TOKEN",
179+
"WORKFLOW_SUMMARY_ENABLED": "true",
180+
},
181+
clear=True,
182+
)
183+
def test_get_env_vars_with_workflow_summary_enabled(self):
184+
"""Test that workflow_summary_enabled is set to True when environment variable is true"""
185+
expected_result = EnvVars(
186+
gh_app_id=None,
187+
gh_app_installation_id=None,
188+
gh_app_private_key_bytes=b"",
189+
gh_app_enterprise_only=False,
190+
gh_token="TOKEN",
191+
ghe=None,
192+
skip_empty_reports=True,
193+
workflow_summary_enabled=True,
194+
)
195+
result = get_env_vars(True)
196+
self.assertEqual(str(result), str(expected_result))
197+
171198

172199
if __name__ == "__main__":
173200
unittest.main()

0 commit comments

Comments
 (0)