Home 使用 Google Apps Script 三步驟免費建立 Github Repo Star Notifier
Post
Cancel

使用 Google Apps Script 三步驟免費建立 Github Repo Star Notifier

使用 Google Apps Script 三步驟免費建立 Github Repo Star Notifier

撰寫 GAS 串接 Github Webhook 轉發按星星 Like 通知到 Line

前言

身為開源專案的維護者,不為錢不為名,只為一個 虛榮心 ;每當看到有新的 ⭐️ 星星時,心中都竊喜不已;花時間花精力做的專案真的有人在用、真的有幫助的有同樣問題的朋友。

[Star History Chart](https://star-history.com/#ZhgChgLi/ZMarkupParser&Date){:target="_blank"}

Star History Chart

因此對 ⭐️ 星星的觀測多少有點強迫症,時不時就刷一下 Github 查看 ⭐️星星 數有沒有增加;我就在想有沒有更主動一點的方式,當有人 按 ⭐️星星 時主動跳通知提示,不需要手動追蹤查詢。

現有工具

首先考慮尋找現有工具達成,到 Github Marketplace 搜尋了一下,有幾個大神做好的工具可以使用。

試了其中幾個效果不如預期,有的已不在運作、有的只能在每 5/10/20 個 ⭐️星星 時發送通知(我只是小小,有 1 個新的 ⭐️ 就很開心了😝)、通知只能發信件但我想要用 SNS 通知。

再加上只是為了「虛榮心」裝一個 App,心裡不太踏實,怕有資安風險問題。

iOS 上的 Github App 或 GitTrends …等等第三方 App 也都不支援此功能。

自己打造 Github Repo Star Notifier

基於以上,其實我們可以直接用 Google Apps Script 免費、快速打造自己的 Github Repo Star Notifier。

2024/10/12 Update

⚠️⚠️⚠️

因 Line Notify 將於 2025/04/01 關閉 ,請參考 我的最新文章「 10 分鐘快速移轉 Line Notify 到 Telegram Bot 通知 改使用 Telegram 串接通知功能。

準備工作

本文以 Line 做為通知媒介,如果你想使用其他通訊軟體通知可以詢問 ChatGPT 如何實現。

詢問 [ChatGPT](https://chat.openai.com){:target="_blank"} 如何實現 Line Notify

詢問 ChatGPT 如何實現 Line Notify

lineToken

  • 前往 Line Notify
  • 登入你的 Line 帳號之後拉到底找到「Generate access token (For developers)」區

  • 點擊「Generate token」

  • Token Name:輸入你想要的機器人頭銜名稱,會顯示在訊息之前 (e.g. Github Repo Notifer: XXXX )
  • 選擇訊息要傳送到的地方:我選擇 1-on-1 chat with LINE Notify 透過 LINE Notify 官方機器人發送訊息給自己。
  • 點擊「Generate token」

  • 選擇「Copy」
  • 並記下 Token,如果日後遺忘需要重新產生,無法再次查看

githubWebhookSecret

  • Copy & 記下此隨機字串

我們會用這組字串做為 Github Webhook 與 Goolge Apps Script 之間的請求驗證媒介。

GAS 限制 ,無法在 doPost(e) 中取得 Headers 內容,因此不能使用 Github Webhook 標準的驗證方式 ,只能手動用 ?secret= Query 做字串匹配驗證。

建立 Google Apps Script

前往 Google Apps Script ,點擊左上角「+ 新專案」。

[**Google Apps Script**](https://script.google.com/home/start){:target="_blank"}

Google Apps Script

點擊左上方「未命名的專案」重新命名專案。

這邊我把專案取名為 My-Github-Repo-Notifier 方便日後辨識。

程式碼輸入區域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// Constant variables
const lineToken = 'XXXX';
// Generate yours line notify bot token: https://notify-bot.line.me/my/
const githubWebhookSecret = "XXXXX";
// Generate yours secret string here: https://www.random.org/strings/?num=1&len=32&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new

// HTTP Get/Post Handler
// 不開放 Get 方法
function doGet(e) {
  return HtmlService.createHtmlOutput("Access Denied!");
}

// Github Webhook 會使用 Post 方法進來
function doPost(e) {
  const content = JSON.parse(e.postData.contents);
  
  // 安全性檢查,確保請求是來自 Github Webhook
  if (verifyGitHubWebhook(e) == false) {
    return HtmlService.createHtmlOutput("Access Denied!");
  }

  // star payload data content["action"] == "started"
  if(content["action"] != "started") {
    return HtmlService.createHtmlOutput("OK!");
  }

  // 組合訊息 
  const message = makeMessageString(content);
  
  // 發送訊息,也可改成發到 Slack,Telegram...
  sendLineNotifyMessage(message);

  return HtmlService.createHtmlOutput("OK!");
}

// Method
// 產生訊息內容
function makeMessageString(content) {
  const repository = content["repository"];
  const repositoryName = repository["name"];
  const repositoryURL = repository["svn_url"];
  const starsCount = repository["stargazers_count"];
  const forksCount = repository["forks_count"];

  const starrer = content["sender"]["login"];

  var message = "🎉🎉「"+starrer+"」starred your「"+repositoryName+"」Repo 🎉🎉\n";
  message += "Current total stars: "+starsCount+"\n";
  message += "Current total forks: "+forksCount+"\n";
  message += repositoryURL;

  return message;
}

// 驗證請求是否來自於 Github Webhook
// 因 GAS 限制 (https://issuetracker.google.com/issues/67764685?pli=1)
// 無法取得 Headers 內容
// 因此不能使用 Github Webhook 標準的驗證方式 (https://docs.github.com/en/webhooks-and-events/webhooks/securing-your-webhooks)
// 只能手動用 ?secret=XXX 做匹配驗證
function verifyGitHubWebhook(e) {
  if (e.parameter["secret"] === githubWebhookSecret) {
    return true
  } else {
    return false
  }
}

// -- Send Message --
// Line
// 其他訊息傳送方式可問 ChatGPT
function sendLineNotifyMessage(message) {
  var url = 'https://notify-api.line.me/api/notify';
  
  var options = {
    method: 'post',
    headers: {
      'Authorization': 'Bearer '+lineToken
    },
    payload: {
      'message': message
    }
  }; 
  UrlFetchApp.fetch(url, options);
}

lineToken & githubWebhookSecret 帶上前一步驟複製的值。

補充 Github Webook 當有人按 Star 時會打進來的資料如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
{
  "action": "created",
  "starred_at": "2023-08-01T03:42:26Z",
  "repository": {
    "id": 602927147,
    "node_id": "R_kgDOI-_wKw",
    "name": "ZMarkupParser",
    "full_name": "ZhgChgLi/ZMarkupParser",
    "private": false,
    "owner": {
      "login": "ZhgChgLi",
      "id": 83232222,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjgzMjMyMjIy",
      "avatar_url": "https://avatars.githubusercontent.com/u/83232222?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/ZhgChgLi",
      "html_url": "https://github.com/ZhgChgLi",
      "followers_url": "https://api.github.com/users/ZhgChgLi/followers",
      "following_url": "https://api.github.com/users/ZhgChgLi/following{/other_user}",
      "gists_url": "https://api.github.com/users/ZhgChgLi/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/ZhgChgLi/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/ZhgChgLi/subscriptions",
      "organizations_url": "https://api.github.com/users/ZhgChgLi/orgs",
      "repos_url": "https://api.github.com/users/ZhgChgLi/repos",
      "events_url": "https://api.github.com/users/ZhgChgLi/events{/privacy}",
      "received_events_url": "https://api.github.com/users/ZhgChgLi/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "html_url": "https://github.com/ZhgChgLi/ZMarkupParser",
    "description": "ZMarkupParser is a pure-Swift library that helps you convert HTML strings into NSAttributedString with customized styles and tags.",
    "fork": false,
    "url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser",
    "forks_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/forks",
    "keys_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/teams",
    "hooks_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/hooks",
    "issue_events_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/issues/events{/number}",
    "events_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/events",
    "assignees_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/assignees{/user}",
    "branches_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/branches{/branch}",
    "tags_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/tags",
    "blobs_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/languages",
    "stargazers_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/stargazers",
    "contributors_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/contributors",
    "subscribers_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/subscribers",
    "subscription_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/subscription",
    "commits_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/contents/{+path}",
    "compare_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/merges",
    "archive_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/downloads",
    "issues_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/issues{/number}",
    "pulls_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/labels{/name}",
    "releases_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/releases{/id}",
    "deployments_url": "https://api.github.com/repos/ZhgChgLi/ZMarkupParser/deployments",
    "created_at": "2023-02-17T08:41:37Z",
    "updated_at": "2023-08-01T03:42:27Z",
    "pushed_at": "2023-08-01T00:07:41Z",
    "git_url": "git://github.com/ZhgChgLi/ZMarkupParser.git",
    "ssh_url": "git@github.com:ZhgChgLi/ZMarkupParser.git",
    "clone_url": "https://github.com/ZhgChgLi/ZMarkupParser.git",
    "svn_url": "https://github.com/ZhgChgLi/ZMarkupParser",
    "homepage": "https://zhgchg.li",
    "size": 27449,
    "stargazers_count": 187,
    "watchers_count": 187,
    "language": "Swift",
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "has_discussions": false,
    "forks_count": 10,
    "mirror_url": null,
    "archived": false,
    "disabled": false,
    "open_issues_count": 2,
    "license": {
      "key": "mit",
      "name": "MIT License",
      "spdx_id": "MIT",
      "url": "https://api.github.com/licenses/mit",
      "node_id": "MDc6TGljZW5zZTEz"
    },
    "allow_forking": true,
    "is_template": false,
    "web_commit_signoff_required": false,
    "topics": [
      "cocoapods",
      "html",
      "html-converter",
      "html-parser",
      "html-renderer",
      "ios",
      "nsattributedstring",
      "swift",
      "swift-package",
      "textfield",
      "uikit",
      "uilabel",
      "uitextview"
    ],
    "visibility": "public",
    "forks": 10,
    "open_issues": 2,
    "watchers": 187,
    "default_branch": "main"
  },
  "organization": {
    "login": "ZhgChgLi",
    "id": 83232222,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjgzMjMyMjIy",
    "url": "https://api.github.com/orgs/ZhgChgLi",
    "repos_url": "https://api.github.com/orgs/ZhgChgLi/repos",
    "events_url": "https://api.github.com/orgs/ZhgChgLi/events",
    "hooks_url": "https://api.github.com/orgs/ZhgChgLi/hooks",
    "issues_url": "https://api.github.com/orgs/ZhgChgLi/issues",
    "members_url": "https://api.github.com/orgs/ZhgChgLi/members{/member}",
    "public_members_url": "https://api.github.com/orgs/ZhgChgLi/public_members{/member}",
    "avatar_url": "https://avatars.githubusercontent.com/u/83232222?v=4",
    "description": "Building a Better World Together."
  },
  "sender": {
    "login": "zhgtest",
    "id": 4601621,
    "node_id": "MDQ6VXNlcjQ2MDE2MjE=",
    "avatar_url": "https://avatars.githubusercontent.com/u/4601621?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/zhgtest",
    "html_url": "https://github.com/zhgtest",
    "followers_url": "https://api.github.com/users/zhgtest/followers",
    "following_url": "https://api.github.com/users/zhgtest/following{/other_user}",
    "gists_url": "https://api.github.com/users/zhgtest/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/zhgtest/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/zhgtest/subscriptions",
    "organizations_url": "https://api.github.com/users/zhgtest/orgs",
    "repos_url": "https://api.github.com/users/zhgtest/repos",
    "events_url": "https://api.github.com/users/zhgtest/events{/privacy}",
    "received_events_url": "https://api.github.com/users/zhgtest/received_events",
    "type": "User",
    "site_admin": false
  }
}

部署

完成程式撰寫之後點擊右上角「部署」->「新增部署作業」:

左側選取類型選擇「網頁應用程式」:

  • 新增說明:隨意輸入,我輸入「 Release
  • 誰可以存取: 請改成「 所有人
  • 點擊「部署」

首次部署,需要點擊「授予存取權」:

跳出帳號選擇 Pop-up 後選擇自己當前的 Gmail 帳號:

出現「Google hasn’t verified this app」因為我們要開發的 App 是給自己用的,不需經過 Google 驗證。

直接點擊「Advanced」->「Go to XXX (unsafe)」->「Allow」即可:

完成部署後可在結果頁面的「網頁應用程式」得到 Request URL,點擊「複製」並記下此 GAS 網址。

⚠️️️ 題外話,請注意如果程式碼有修改需要更新部署才會生效⚠️

要使更改的程式碼生效,同樣點擊右上角「部署」-> 選擇「管理部署作業」->選擇右上角的「✏️」->版本選擇「建立新版本」->點擊「部署」。

即可完成程式碼更新部署。

Github Webhook 設定

  • 回到 Github
  • 我們可以對 Organizations (裡面所有 Repo)或單個 Repo 設定 Webhook,監聽新的 ⭐️ 星星

進入 Organizations / Repo -> 「Settings」-> 左側找到「Webhooks」-> 「Add webhook」:

  • Payload URL 輸入 GAS 網址 並在網址後面手動加上我們自己的安全驗證字串 ?secret=githubWebhookSecret 。 例如你的 GAS 網址https://script.google.com/macros/s/XXX/execgithubWebhookSecret123456 ;則 網址即為: https://script.google.com/macros/s/XXX/exec?secret=123456
  • Content type: 選擇 application/json
  • Which events would you like to trigger this webhook? 選擇「 Let me select individual events. ⚠️️取消勾選「 Pushes ️️️️⚠️勾選「 Watches 」,請注意不是「 Stars 」(但 Stars 也是監控點擊星星的狀態,如果用 Stars GAS 的 action 判斷也需要調整 )
  • 選擇「 Active
  • 點擊「Add webhook」
  • 完成設定

🚀測試

回到 設定的 Organizations Repo / Repo 上點擊「Star」或先 un-star 再重新 「Star」:

就會收到推播通知囉!

收工!🎉🎉🎉🎉

工商

有任何問題及指教歡迎 與我聯絡

===

View the English version of this article here.

本文首次發表於 Medium ➡️ 前往查看



This post is licensed under CC BY 4.0 by the author.

遊記 2023 東京 5 日自由行

POC App End-to-End Testing Local Snapshot API Mock Server