github-metrics-notify.yml 14 KB

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