Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
statuses: write
steps:
- name: Checkout Code
uses: actions/checkout@v5.0.0
uses: actions/checkout@v5
with:
# Full git history is needed to get a proper
# list of changed files within `super-linter`
Expand All @@ -28,7 +28,7 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-test.txt
- name: Lint Code Base
uses: super-linter/super-linter@5119dcd8011e92182ce8219d9e9efc82f16fddb6
uses: super-linter/super-linter@v6
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
matrix:
python-version: [3.11, 3.12, 3.13]
steps:
- uses: actions/checkout@v5.0.0
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/use-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
packages: read
steps:
- name: Checkout code
uses: actions/checkout@v5.0.0
uses: actions/checkout@v5
- name: Run stale_repos tool
uses: docker://ghcr.io/github/stale_repos:v3
env:
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe
| `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"` |
| `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. |
| `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. |

### Example workflow

Expand Down Expand Up @@ -124,6 +125,40 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
```

### Using Workflow Summary

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.

```yaml
name: stale repo identifier

on:
workflow_dispatch:
schedule:
- cron: "3 2 1 * *"

permissions:
contents: read

jobs:
build:
name: stale repo identifier
runs-on: ubuntu-latest

steps:
- name: Run stale_repos tool
uses: github/stale-repos@v3
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
ORGANIZATION: ${{ secrets.ORGANIZATION }}
EXEMPT_TOPICS: "keep,template"
INACTIVE_DAYS: 365
ADDITIONAL_METRICS: "release,pr"
WORKFLOW_SUMMARY_ENABLED: true
```

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.

### Example stale_repos.md output

```markdown
Expand Down
7 changes: 7 additions & 0 deletions env.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class EnvVars:
ghe (str): The GitHub Enterprise URL to use for authentication
skip_empty_reports (bool): If true, Skips report creation when no stale
repositories are identified
workflow_summary_enabled (bool): If true, adds the markdown report to GitHub
Actions workflow summary
"""

def __init__(
Expand All @@ -39,6 +41,7 @@ def __init__(
gh_token: str | None,
ghe: str | None,
skip_empty_reports: bool,
workflow_summary_enabled: bool,
):
self.gh_app_id = gh_app_id
self.gh_app_installation_id = gh_app_installation_id
Expand All @@ -47,6 +50,7 @@ def __init__(
self.gh_token = gh_token
self.ghe = ghe
self.skip_empty_reports = skip_empty_reports
self.workflow_summary_enabled = workflow_summary_enabled

def __repr__(self):
return (
Expand All @@ -58,6 +62,7 @@ def __repr__(self):
f"{self.gh_token},"
f"{self.ghe},"
f"{self.skip_empty_reports},"
f"{self.workflow_summary_enabled},"
)


Expand Down Expand Up @@ -120,6 +125,7 @@ def get_env_vars(
ghe = os.getenv("GH_ENTERPRISE_URL")
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")
skip_empty_reports = get_bool_env_var("SKIP_EMPTY_REPORTS", True)
workflow_summary_enabled = get_bool_env_var("WORKFLOW_SUMMARY_ENABLED")

if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
raise ValueError(
Expand All @@ -142,4 +148,5 @@ def get_env_vars(
gh_token,
ghe,
skip_empty_reports,
workflow_summary_enabled,
)
112 changes: 79 additions & 33 deletions stale_repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def main(): # pragma: no cover
ghe = env_vars.ghe
gh_app_enterprise_only = env_vars.gh_app_enterprise_only
skip_empty_reports = env_vars.skip_empty_reports
workflow_summary_enabled = env_vars.workflow_summary_enabled

# Auth to GitHub.com or GHE
github_connection = auth.auth_to_github(
Expand Down Expand Up @@ -72,7 +73,12 @@ def main(): # pragma: no cover

if inactive_repos or not skip_empty_reports:
output_to_json(inactive_repos)
write_to_markdown(inactive_repos, inactive_days_threshold, additional_metrics)
write_to_markdown(
inactive_repos,
inactive_days_threshold,
additional_metrics,
workflow_summary_enabled,
)
else:
print("Reporting skipped; no stale repos found.")

Expand Down Expand Up @@ -236,7 +242,11 @@ def get_active_date(repo):


def write_to_markdown(
inactive_repos, inactive_days_threshold, additional_metrics=None, file=None
inactive_repos,
inactive_days_threshold,
additional_metrics=None,
workflow_summary_enabled=False,
file=None,
):
"""Write the list of inactive repos to a markdown file.

Expand All @@ -246,48 +256,84 @@ def write_to_markdown(
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.
workflow_summary_enabled: If True, adds the report to GitHub Actions workflow summary.
file: A file object to write to. If None, a new file will be created.

"""
inactive_repos = sorted(
inactive_repos, key=lambda x: x["days_inactive"], reverse=True
)

# Generate markdown content
content = generate_markdown_content(
inactive_repos, inactive_days_threshold, additional_metrics
)

# Write to file
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 |"
markdown_file.write(content)
print("Wrote stale repos to stale_repos.md")

# Write to GitHub step summary if enabled
if workflow_summary_enabled and os.environ.get("GITHUB_STEP_SUMMARY"):
with open(
os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8"
) as summary_file:
summary_file.write(content)
print("Added stale repos to workflow summary")


def generate_markdown_content(
inactive_repos, inactive_days_threshold, additional_metrics=None
):
"""Generate markdown content for the inactive repos report.

Args:
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.

Returns:
str: The generated markdown content.
"""
content = "# Inactive Repositories\n\n"
content += (
f"The following repos have not had a push event for more than "
f"{inactive_days_threshold} days:\n\n"
)
content += "| Repository URL | Days Inactive | Last Push Date | Visibility |"

# Include additional metrics columns if configured
if additional_metrics:
if "release" in additional_metrics:
content += " Days Since Last Release |"
if "pr" in additional_metrics:
content += " Days Since Last PR |"
content += "\n| --- | --- | --- | --- |"
if additional_metrics:
if "release" in additional_metrics:
content += " --- |"
if "pr" in additional_metrics:
content += " --- |"
content += "\n"

for repo_data in inactive_repos:
content += (
f"| {repo_data['url']} "
f"| {repo_data['days_inactive']} "
f"| {repo_data['last_push_date']} "
f"| {repo_data['visibility']} |"
)
# Include additional metrics columns if configured
if additional_metrics:
if "release" in additional_metrics:
markdown_file.write(" Days Since Last Release |")
content += f" {repo_data['days_since_last_release']} |"
if "pr" in additional_metrics:
markdown_file.write(" Days Since Last PR |")
markdown_file.write("\n| --- | --- | --- | --- |")
if additional_metrics:
if "release" in additional_metrics:
markdown_file.write(" --- |")
if "pr" in additional_metrics:
markdown_file.write(" --- |")
markdown_file.write("\n")
for repo_data in inactive_repos:
markdown_file.write(
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")
content += f" {repo_data['days_since_last_pr']} |"
content += "\n"

return content


def output_to_json(inactive_repos, file=None):
Expand Down
27 changes: 27 additions & 0 deletions test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def test_get_env_vars_with_github_app(self):
gh_token="",
ghe="",
skip_empty_reports=True,
workflow_summary_enabled=False,
)
result = get_env_vars(True)
self.assertEqual(str(result), str(expected_result))
Expand All @@ -79,6 +80,7 @@ def test_get_env_vars_with_token(self):
gh_token=TOKEN,
ghe="",
skip_empty_reports=True,
workflow_summary_enabled=False,
)
result = get_env_vars(True)
self.assertEqual(str(result), str(expected_result))
Expand Down Expand Up @@ -119,6 +121,7 @@ def test_get_env_vars_optional_values(self):
gh_token=TOKEN,
ghe="",
skip_empty_reports=False,
workflow_summary_enabled=False,
)
result = get_env_vars(True)
self.assertEqual(str(result), str(expected_result))
Expand All @@ -143,6 +146,7 @@ def test_get_env_vars_optionals_are_defaulted(self):
gh_token="TOKEN",
ghe=None,
skip_empty_reports=True,
workflow_summary_enabled=False,
)
result = get_env_vars(True)
self.assertEqual(str(result), str(expected_result))
Expand All @@ -168,6 +172,29 @@ def test_get_env_vars_auth_with_github_app_installation_missing_inputs(self):
"GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set",
)

@patch.dict(
os.environ,
{
"GH_TOKEN": "TOKEN",
"WORKFLOW_SUMMARY_ENABLED": "true",
},
clear=True,
)
def test_get_env_vars_with_workflow_summary_enabled(self):
"""Test that workflow_summary_enabled is set to True when environment variable is true"""
expected_result = EnvVars(
gh_app_id=None,
gh_app_installation_id=None,
gh_app_private_key_bytes=b"",
gh_app_enterprise_only=False,
gh_token="TOKEN",
ghe=None,
skip_empty_reports=True,
workflow_summary_enabled=True,
)
result = get_env_vars(True)
self.assertEqual(str(result), str(expected_result))


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