GitHub Metrics Notification #129
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# .github/workflows/github-metrics-notify.yml | |
name: GitHub Metrics Notification | |
# Grants specific permissions to the GITHUB_TOKEN | |
permissions: | |
contents: write # Allows pushing changes to the repository | |
issues: read # Optional: if you're interacting with issues | |
pull-requests: write # Optional: if you're interacting with pull requests | |
# Triggers the workflow on specific GitHub events and schedules it as a backup | |
on: | |
# Trigger on repository star events | |
watch: | |
types: [started, deleted] # 'started' for star, 'deleted' for unstar | |
# Trigger on repository forks | |
fork: | |
# Trigger on issue events | |
issues: | |
types: [opened, closed, reopened, edited] | |
# Trigger on pull request events | |
pull_request: | |
types: [opened, closed, reopened, edited] | |
# Trigger on release events | |
release: | |
types: [published, edited, prereleased, released] | |
# Trigger on push events to the main branch | |
push: | |
branches: | |
- main | |
# Scheduled backup trigger every hour for followers/subscribers | |
schedule: | |
- cron: '0 */1 * * *' # Every hour at minute 0 | |
# Allows manual triggering | |
workflow_dispatch: | |
jobs: | |
notify_metrics: | |
runs-on: ubuntu-latest | |
steps: | |
# Step 1: Checkout the repository | |
- name: Checkout Repository | |
uses: actions/checkout@v3 | |
with: | |
persist-credentials: true # Enables Git commands to use GITHUB_TOKEN | |
fetch-depth: 0 # Fetch all history for accurate metric tracking | |
# Step 2: Set Up Python Environment | |
- name: Set Up Python | |
uses: actions/setup-python@v4 | |
with: | |
python-version: '3.x' # Specify the Python version | |
# Step 3: Install Python Dependencies | |
- name: Install Dependencies | |
run: | | |
python -m pip install --upgrade pip | |
pip install requests | |
# Step 4: Fetch and Compare Metrics | |
- name: Fetch and Compare Metrics | |
id: fetch_metrics | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Built-in secret provided by GitHub Actions | |
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} # Your Discord webhook URL | |
GITHUB_EVENT_NAME: ${{ github.event_name }} # To determine if run is manual or triggered by an event | |
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} # Dynamic repository owner | |
run: | | |
python3 - <<'EOF' > fetch_metrics.out | |
import os | |
import requests | |
import json | |
from datetime import datetime | |
# Configuration | |
REPO_OWNER = os.getenv('GITHUB_REPOSITORY_OWNER') | |
REPO_NAME = os.getenv('GITHUB_REPOSITORY').split('/')[-1] | |
METRICS_FILE = ".github/metrics.json" | |
# Ensure .github directory exists | |
os.makedirs(os.path.dirname(METRICS_FILE), exist_ok=True) | |
# GitHub API Headers | |
headers = { | |
"Authorization": f"token {os.getenv('GITHUB_TOKEN')}", | |
"Accept": "application/vnd.github.v3+json" | |
} | |
# Function to fetch closed issues count using GitHub Search API | |
def fetch_closed_issues(owner, repo, headers): | |
search_api = f"https://api.github.com/search/issues?q=repo:{owner}/{repo}+type:issue+state:closed" | |
try: | |
response = requests.get(search_api, headers=headers) | |
response.raise_for_status() | |
data = response.json() | |
return data.get('total_count', 0) | |
except requests.exceptions.RequestException as e: | |
print(f"Error fetching closed issues count: {e}") | |
return 0 | |
# Fetch current metrics from GitHub API | |
repo_api = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}" | |
try: | |
response = requests.get(repo_api, headers=headers) | |
response.raise_for_status() | |
repo_data = response.json() | |
stars = repo_data.get('stargazers_count', 0) | |
forks = repo_data.get('forks_count', 0) | |
followers = repo_data.get('subscribers_count', 0) | |
open_issues = repo_data.get('open_issues_count', 0) | |
closed_issues = fetch_closed_issues(REPO_OWNER, REPO_NAME, headers) | |
except requests.exceptions.RequestException as e: | |
print(f"Error fetching repository data: {e}") | |
exit(1) | |
# Function to load previous metrics with error handling | |
def load_previous_metrics(file_path): | |
try: | |
with open(file_path, 'r') as file: | |
return json.load(file) | |
except json.decoder.JSONDecodeError: | |
print("metrics.json is corrupted or empty. Reinitializing.") | |
return { | |
"stars": 0, | |
"forks": 0, | |
"followers": 0, | |
"open_issues": 0, | |
"closed_issues": 0 | |
} | |
except FileNotFoundError: | |
return { | |
"stars": 0, | |
"forks": 0, | |
"followers": 0, | |
"open_issues": 0, | |
"closed_issues": 0 | |
} | |
# Load previous metrics | |
prev_metrics = load_previous_metrics(METRICS_FILE) | |
is_initial_run = not os.path.exists(METRICS_FILE) | |
# Determine changes (both increases and decreases) | |
changes = {} | |
metrics = ["stars", "forks", "followers", "open_issues", "closed_issues"] | |
current_metrics = { | |
"stars": stars, | |
"forks": forks, | |
"followers": followers, | |
"open_issues": open_issues, | |
"closed_issues": closed_issues | |
} | |
for metric in metrics: | |
current = current_metrics.get(metric, 0) | |
previous = prev_metrics.get(metric, 0) | |
if current != previous: | |
changes[metric] = current - previous | |
# Update metrics file | |
with open(METRICS_FILE, 'w') as file: | |
json.dump(current_metrics, file) | |
# Determine if a notification should be sent | |
event_name = os.getenv('GITHUB_EVENT_NAME') | |
send_notification = False | |
no_changes = False | |
initial_setup = False | |
if is_initial_run: | |
if event_name == 'workflow_dispatch': | |
# Manual run: Send notification for initial setup | |
send_notification = True | |
initial_setup = True | |
elif event_name == 'watch' and changes.get('stars'): | |
# Initial run triggered by a star event: Send notification | |
send_notification = True | |
else: | |
# Event-triggered runs: Do not send notification on initial setup | |
send_notification = False | |
else: | |
if event_name == 'workflow_dispatch': | |
# Manual run: Always send notification | |
send_notification = True | |
if not changes: | |
no_changes = True | |
elif event_name == 'watch': | |
# Star event: Send notification only if stars changed | |
if changes.get('stars'): | |
send_notification = True | |
else: | |
# Scheduled run or other events: Send notification only if there are changes | |
if changes: | |
send_notification = True | |
if send_notification: | |
# Prepare Discord notification payload | |
payload = { | |
"embeds": [ | |
{ | |
"title": "📈 GitHub Repository Metrics Updated", | |
"url": f"https://github.com/{REPO_OWNER}/{REPO_NAME}", # Link back to the repository | |
"color": 0x7289DA, # Discord blurple color | |
"thumbnail": { | |
"url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo | |
}, | |
"fields": [ | |
{ | |
"name": "📂 Repository", | |
"value": f"[{REPO_OWNER}/{REPO_NAME}](https://github.com/{REPO_OWNER}/{REPO_NAME})", | |
"inline": False | |
}, | |
{ | |
"name": "⭐ Stars", | |
"value": f"{stars}", | |
"inline": True | |
}, | |
{ | |
"name": "🍴 Forks", | |
"value": f"{forks}", | |
"inline": True | |
}, | |
{ | |
"name": "👥 Followers", | |
"value": f"{followers}", | |
"inline": True | |
}, | |
{ | |
"name": "🐛 Open Issues", | |
"value": f"{open_issues}", | |
"inline": True | |
}, | |
{ | |
"name": "🔒 Closed Issues", | |
"value": f"{closed_issues}", | |
"inline": True | |
}, | |
], | |
"footer": { | |
"text": "GitHub Metrics Monitor", | |
"icon_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo | |
}, | |
"timestamp": datetime.utcnow().isoformat() # Adds a timestamp to the embed | |
} | |
] | |
} | |
if initial_setup: | |
# Add a field indicating initial setup | |
payload["embeds"][0]["fields"].append({ | |
"name": "⚙️ Initial Setup", | |
"value": "Metrics tracking has been initialized.", | |
"inline": False | |
}) | |
elif changes: | |
# Add fields for each updated metric | |
for metric, count in changes.items(): | |
emoji = { | |
"stars": "⭐", | |
"forks": "🍴", | |
"followers": "👥", | |
"open_issues": "🐛", | |
"closed_issues": "🔒" | |
}.get(metric, "") | |
change_symbol = "+" if count > 0 else "" | |
payload["embeds"][0]["fields"].append({ | |
"name": f"{emoji} {metric.replace('_', ' ').capitalize()} (Change)", | |
"value": f"{change_symbol}{count}", | |
"inline": True | |
}) | |
elif no_changes: | |
# Indicate that there were no changes during a manual run | |
payload["embeds"][0]["fields"].append({ | |
"name": "✅ No Changes", | |
"value": "No updates to metrics since the last check.", | |
"inline": False | |
}) | |
# Save payload to a temporary file for the next step | |
with open('payload.json', 'w') as f: | |
json.dump(payload, f) | |
# Output whether to send notification | |
if initial_setup or changes or no_changes: | |
print("SEND_NOTIFICATION=true") | |
else: | |
print("SEND_NOTIFICATION=false") | |
else: | |
print("SEND_NOTIFICATION=false") | |
EOF | |
# Step 5: Ensure .gitignore Ignores Temporary Files | |
- name: Ensure .gitignore Ignores Temporary Files | |
run: | | |
# Check if .gitignore exists; if not, create it | |
if [ ! -f .gitignore ]; then | |
touch .gitignore | |
fi | |
# Add 'fetch_metrics.out' if not present | |
if ! grep -Fxq "fetch_metrics.out" .gitignore; then | |
echo "fetch_metrics.out" >> .gitignore | |
echo "Added 'fetch_metrics.out' to .gitignore" | |
else | |
echo "'fetch_metrics.out' already present in .gitignore" | |
fi | |
# Add 'payload.json' if not present | |
if ! grep -Fxq "payload.json" .gitignore; then | |
echo "payload.json" >> .gitignore | |
echo "Added 'payload.json' to .gitignore" | |
else | |
echo "'payload.json' already present in .gitignore" | |
fi | |
# Step 6: Decide Whether to Send Notification | |
- name: Check if Notification Should Be Sent | |
id: decide_notification | |
run: | | |
if grep -q "SEND_NOTIFICATION=true" fetch_metrics.out; then | |
echo "send=true" >> $GITHUB_OUTPUT | |
else | |
echo "send=false" >> $GITHUB_OUTPUT | |
fi | |
shell: bash | |
# Step 7: Send Discord Notification using curl | |
- name: Send Discord Notification | |
if: steps.decide_notification.outputs.send == 'true' | |
run: | | |
curl -H "Content-Type: application/json" -d @payload.json ${{ secrets.DISCORD_WEBHOOK_URL }} | |
# Step 8: Commit and Push Updated metrics.json and .gitignore | |
- name: Commit and Push Changes | |
if: steps.decide_notification.outputs.send == 'true' | |
run: | | |
git config --global user.name "GitHub Actions" | |
git config --global user.email "[email protected]" | |
# Stage metrics.json | |
git add .github/metrics.json | |
# Stage .gitignore only if it was modified | |
if git diff --name-only | grep -q "^\.gitignore$"; then | |
git add .gitignore | |
else | |
echo ".gitignore not modified" | |
fi | |
# Commit changes if there are any | |
git commit -m "Update metrics.json and ensure temporary files are ignored [skip ci]" || echo "No changes to commit" | |
# Push changes to the main branch | |
git push origin main # Replace 'main' with your default branch if different | |
# Step 9: Clean Up Temporary Files | |
- name: Clean Up Temporary Files | |
if: always() | |
run: | | |
rm -f fetch_metrics.out payload.json |