Skip to content

GitHub Metrics Notification #133

GitHub Metrics Notification

GitHub Metrics Notification #133

# .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