github-metrics-notify.yml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. # .github/workflows/github-metrics-notify.yml
  2. name: GitHub Metrics Notification
  3. # Grants specific permissions to the GITHUB_TOKEN
  4. permissions:
  5. contents: write # Allows pushing changes to the repository
  6. issues: read # Optional: if you're interacting with issues
  7. pull-requests: write # Optional: if you're interacting with pull requests
  8. # Triggers the workflow on specific GitHub events and schedules it as a backup
  9. on:
  10. # Trigger on repository star events
  11. watch:
  12. types: [started, deleted] # 'started' for star, 'deleted' for unstar
  13. # Trigger on repository forks
  14. fork:
  15. # Trigger on issue events
  16. issues:
  17. types: [opened, closed, reopened, edited]
  18. # Trigger on pull request events
  19. pull_request:
  20. types: [opened, closed, reopened, edited]
  21. # Trigger on release events
  22. release:
  23. types: [published, edited, prereleased, released]
  24. # Trigger on push events to the main branch
  25. push:
  26. branches:
  27. - main
  28. # Scheduled backup trigger every hour for followers/subscribers
  29. schedule:
  30. - cron: '0 */1 * * *' # Every hour at minute 0
  31. # Allows manual triggering
  32. workflow_dispatch:
  33. jobs:
  34. notify_metrics:
  35. runs-on: ubuntu-latest
  36. steps:
  37. # Step 1: Checkout the repository
  38. - name: Checkout Repository
  39. uses: actions/checkout@v3
  40. with:
  41. persist-credentials: true # Enables Git commands to use GITHUB_TOKEN
  42. fetch-depth: 0 # Fetch all history for accurate metric tracking
  43. # Step 2: Set Up Python Environment
  44. - name: Set Up Python
  45. uses: actions/setup-python@v4
  46. with:
  47. python-version: '3.x' # Specify the Python version
  48. # Step 3: Install Python Dependencies
  49. - name: Install Dependencies
  50. run: |
  51. python -m pip install --upgrade pip
  52. pip install requests
  53. # Step 4: Fetch and Compare Metrics
  54. - name: Fetch and Compare Metrics
  55. id: fetch_metrics
  56. env:
  57. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Built-in secret provided by GitHub Actions
  58. DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} # Your Discord webhook URL
  59. GITHUB_EVENT_NAME: ${{ github.event_name }} # To determine if run is manual or triggered by an event
  60. GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} # Dynamic repository owner
  61. run: |
  62. python3 - <<'EOF' > fetch_metrics.out
  63. import os
  64. import requests
  65. import json
  66. from datetime import datetime
  67. # Configuration
  68. REPO_OWNER = os.getenv('GITHUB_REPOSITORY_OWNER')
  69. REPO_NAME = os.getenv('GITHUB_REPOSITORY').split('/')[-1]
  70. METRICS_FILE = ".github/metrics.json"
  71. # Ensure .github directory exists
  72. os.makedirs(os.path.dirname(METRICS_FILE), exist_ok=True)
  73. # GitHub API Headers
  74. headers = {
  75. "Authorization": f"token {os.getenv('GITHUB_TOKEN')}",
  76. "Accept": "application/vnd.github.v3+json"
  77. }
  78. # Function to fetch closed issues count using GitHub Search API
  79. def fetch_closed_issues(owner, repo, headers):
  80. search_api = f"https://api.github.com/search/issues?q=repo:{owner}/{repo}+type:issue+state:closed"
  81. try:
  82. response = requests.get(search_api, headers=headers)
  83. response.raise_for_status()
  84. data = response.json()
  85. return data.get('total_count', 0)
  86. except requests.exceptions.RequestException as e:
  87. print(f"Error fetching closed issues count: {e}")
  88. return 0
  89. # Fetch current metrics from GitHub API
  90. repo_api = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}"
  91. try:
  92. response = requests.get(repo_api, headers=headers)
  93. response.raise_for_status()
  94. repo_data = response.json()
  95. stars = repo_data.get('stargazers_count', 0)
  96. forks = repo_data.get('forks_count', 0)
  97. followers = repo_data.get('subscribers_count', 0)
  98. open_issues = repo_data.get('open_issues_count', 0)
  99. closed_issues = fetch_closed_issues(REPO_OWNER, REPO_NAME, headers)
  100. except requests.exceptions.RequestException as e:
  101. print(f"Error fetching repository data: {e}")
  102. exit(1)
  103. # Function to load previous metrics with error handling
  104. def load_previous_metrics(file_path):
  105. try:
  106. with open(file_path, 'r') as file:
  107. return json.load(file)
  108. except json.decoder.JSONDecodeError:
  109. print("metrics.json is corrupted or empty. Reinitializing.")
  110. return {
  111. "stars": 0,
  112. "forks": 0,
  113. "followers": 0,
  114. "open_issues": 0,
  115. "closed_issues": 0
  116. }
  117. except FileNotFoundError:
  118. return {
  119. "stars": 0,
  120. "forks": 0,
  121. "followers": 0,
  122. "open_issues": 0,
  123. "closed_issues": 0
  124. }
  125. # Load previous metrics
  126. prev_metrics = load_previous_metrics(METRICS_FILE)
  127. is_initial_run = not os.path.exists(METRICS_FILE)
  128. # Determine changes (both increases and decreases)
  129. changes = {}
  130. metrics = ["stars", "forks", "followers", "open_issues", "closed_issues"]
  131. current_metrics = {
  132. "stars": stars,
  133. "forks": forks,
  134. "followers": followers,
  135. "open_issues": open_issues,
  136. "closed_issues": closed_issues
  137. }
  138. for metric in metrics:
  139. current = current_metrics.get(metric, 0)
  140. previous = prev_metrics.get(metric, 0)
  141. if current != previous:
  142. changes[metric] = current - previous
  143. # Update metrics file
  144. with open(METRICS_FILE, 'w') as file:
  145. json.dump(current_metrics, file)
  146. # Determine if a notification should be sent
  147. event_name = os.getenv('GITHUB_EVENT_NAME')
  148. send_notification = False
  149. no_changes = False
  150. initial_setup = False
  151. if is_initial_run:
  152. if event_name == 'workflow_dispatch':
  153. # Manual run: Send notification for initial setup
  154. send_notification = True
  155. initial_setup = True
  156. elif event_name == 'watch' and changes.get('stars'):
  157. # Initial run triggered by a star event: Send notification
  158. send_notification = True
  159. else:
  160. # Event-triggered runs: Do not send notification on initial setup
  161. send_notification = False
  162. else:
  163. if event_name == 'workflow_dispatch':
  164. # Manual run: Always send notification
  165. send_notification = True
  166. if not changes:
  167. no_changes = True
  168. elif event_name == 'watch':
  169. # Star event: Send notification only if stars changed
  170. if changes.get('stars'):
  171. send_notification = True
  172. else:
  173. # Scheduled run or other events: Send notification only if there are changes
  174. if changes:
  175. send_notification = True
  176. if send_notification:
  177. triggering_actor = os.getenv('GITHUB_ACTOR', 'Unknown')
  178. # Prepare Discord notification payload
  179. payload = {
  180. "embeds": [
  181. {
  182. "title": "📈 GitHub Repository Metrics Updated",
  183. "url": f"https://github.com/{REPO_OWNER}/{REPO_NAME}", # Link back to the repository
  184. "color": 0x7289DA, # Discord blurple color
  185. "thumbnail": {
  186. "url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo
  187. },
  188. "fields": [
  189. {
  190. "name": "📂 Repository",
  191. "value": f"[{REPO_OWNER}/{REPO_NAME}](https://github.com/{REPO_OWNER}/{REPO_NAME})",
  192. "inline": False
  193. },
  194. {
  195. "name": "⭐ Stars",
  196. "value": f"{stars}",
  197. "inline": True
  198. },
  199. {
  200. "name": "🍴 Forks",
  201. "value": f"{forks}",
  202. "inline": True
  203. },
  204. {
  205. "name": "👥 Followers",
  206. "value": f"{followers}",
  207. "inline": True
  208. },
  209. {
  210. "name": "🐛 Open Issues",
  211. "value": f"{open_issues}",
  212. "inline": True
  213. },
  214. {
  215. "name": "🔒 Closed Issues",
  216. "value": f"{closed_issues}",
  217. "inline": True
  218. },
  219. {
  220. "name": "👤 Triggered By",
  221. "value": triggering_actor,
  222. "inline": False
  223. },
  224. ],
  225. "footer": {
  226. "text": "GitHub Metrics Monitor",
  227. "icon_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" # GitHub logo
  228. },
  229. "timestamp": datetime.utcnow().isoformat() # Adds a timestamp to the embed
  230. }
  231. ]
  232. }
  233. if initial_setup:
  234. # Add a field indicating initial setup
  235. payload["embeds"][0]["fields"].append({
  236. "name": "⚙️ Initial Setup",
  237. "value": "Metrics tracking has been initialized.",
  238. "inline": False
  239. })
  240. elif changes:
  241. # Add fields for each updated metric
  242. for metric, count in changes.items():
  243. emoji = {
  244. "stars": "⭐",
  245. "forks": "🍴",
  246. "followers": "👥",
  247. "open_issues": "🐛",
  248. "closed_issues": "🔒"
  249. }.get(metric, "")
  250. change_symbol = "+" if count > 0 else ""
  251. payload["embeds"][0]["fields"].append({
  252. "name": f"{emoji} {metric.replace('_', ' ').capitalize()} (Change)",
  253. "value": f"{change_symbol}{count}",
  254. "inline": True
  255. })
  256. elif no_changes:
  257. # Indicate that there were no changes during a manual run
  258. payload["embeds"][0]["fields"].append({
  259. "name": "✅ No Changes",
  260. "value": "No updates to metrics since the last check.",
  261. "inline": False
  262. })
  263. # Save payload to a temporary file for the next step
  264. with open('payload.json', 'w') as f:
  265. json.dump(payload, f)
  266. # Output whether to send notification
  267. if initial_setup or changes or no_changes:
  268. print("SEND_NOTIFICATION=true")
  269. else:
  270. print("SEND_NOTIFICATION=false")
  271. else:
  272. print("SEND_NOTIFICATION=false")
  273. EOF
  274. # Step 5: Ensure .gitignore Ignores Temporary Files
  275. - name: Ensure .gitignore Ignores Temporary Files
  276. run: |
  277. # Check if .gitignore exists; if not, create it
  278. if [ ! -f .gitignore ]; then
  279. touch .gitignore
  280. fi
  281. # Add 'fetch_metrics.out' if not present
  282. if ! grep -Fxq "fetch_metrics.out" .gitignore; then
  283. echo "fetch_metrics.out" >> .gitignore
  284. echo "Added 'fetch_metrics.out' to .gitignore"
  285. else
  286. echo "'fetch_metrics.out' already present in .gitignore"
  287. fi
  288. # Add 'payload.json' if not present
  289. if ! grep -Fxq "payload.json" .gitignore; then
  290. echo "payload.json" >> .gitignore
  291. echo "Added 'payload.json' to .gitignore"
  292. else
  293. echo "'payload.json' already present in .gitignore"
  294. fi
  295. # Step 6: Decide Whether to Send Notification
  296. - name: Check if Notification Should Be Sent
  297. id: decide_notification
  298. run: |
  299. if grep -q "SEND_NOTIFICATION=true" fetch_metrics.out; then
  300. echo "send=true" >> $GITHUB_OUTPUT
  301. else
  302. echo "send=false" >> $GITHUB_OUTPUT
  303. fi
  304. shell: bash
  305. # Step 7: Send Discord Notification using curl
  306. - name: Send Discord Notification
  307. if: steps.decide_notification.outputs.send == 'true'
  308. run: |
  309. curl -H "Content-Type: application/json" -d @payload.json ${{ secrets.DISCORD_WEBHOOK_URL }}
  310. # Step 8: Commit and Push Updated metrics.json and .gitignore
  311. - name: Commit and Push Changes
  312. if: steps.decide_notification.outputs.send == 'true'
  313. run: |
  314. git config --global user.name "GitHub Actions"
  315. git config --global user.email "actions@github.com"
  316. # Stage metrics.json
  317. git add .github/metrics.json
  318. # Stage .gitignore only if it was modified
  319. if git diff --name-only | grep -q "^\.gitignore$"; then
  320. git add .gitignore
  321. else
  322. echo ".gitignore not modified"
  323. fi
  324. # Commit changes if there are any
  325. git commit -m "Update metrics.json and ensure temporary files are ignored [skip ci]" || echo "No changes to commit"
  326. # Push changes to the main branch
  327. git push origin main # Replace 'main' with your default branch if different
  328. # Step 9: Clean Up Temporary Files
  329. - name: Clean Up Temporary Files
  330. if: always()
  331. run: |
  332. rm -f fetch_metrics.out payload.json