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

ZhgChgLi
ZRealm Robotic Process Automation
27 min readAug 1, 2023

--

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

前言

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

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 如何實現 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

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

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

程式碼輸入區域:

// 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 時會打進來的資料如下:

{
"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」:

就會收到推播通知囉!

收工!🎉🎉🎉🎉

工商

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

--

--

ZhgChgLi
ZRealm Robotic Process Automation

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing. https://link.zhgchg.li/