Home Using Google Apps Script to Create a Free Github Repo Star Notifier in Three Steps
Post
Cancel

Using Google Apps Script to Create a Free Github Repo Star Notifier in Three Steps

Using Google Apps Script to Create a Free Github Repo Star Notifier in Three Steps

Writing GAS to connect Github Webhook and forward star notifications to Line

Introduction

As a maintainer of open-source projects, it’s not for money or fame, but for a sense of vanity; every time I see a new ⭐️ star, I feel a secret joy in my heart. It means that the project I spent time and effort on is really being used and is helpful to friends with the same problems.

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

Star History Chart

Therefore, I have a bit of an obsession with observing ⭐️ stars, frequently refreshing Github to see if the number of ⭐️ stars has increased. I wondered if there was a more proactive way to get notifications when someone stars the repo, without having to manually check.

Existing Tools

First, I considered looking for existing tools to achieve this. I searched Github Marketplace and found some tools created by experts.

I tried a few of them, but the results were not as expected. Some were no longer working, some only sent notifications every 5/10/20 stars (I’m just a small developer, even 1 new ⭐️ makes me happy 😝), and some only sent email notifications, but I wanted SNS notifications.

Moreover, installing an app just for “vanity” didn’t feel right, and I was concerned about potential security risks.

The Github App on iOS or third-party apps like GitTrends also do not support this feature.

Creating Your Own Github Repo Star Notifier

Based on the above, we can actually use Google Apps Script to quickly and freely create our own Github Repo Star Notifier.

Preparation

This article uses Line as the notification medium. If you want to use other messaging apps, you can ask ChatGPT how to implement it.

Ask [ChatGPT](https://chat.openai.com){:target="_blank"} how to implement Line Notify

Ask ChatGPT how to implement Line Notify

lineToken:

  • Go to Line Notify
  • After logging into your Line account, scroll to the bottom to find the “Generate access token (For developers)” section

  • Click “Generate token”

  • Token Name: Enter the title name you want for the bot, which will be displayed before the message (e.g. Github Repo Notifier: XXXX)
  • Choose where the message will be sent: I chose 1-on-1 chat with LINE Notify to send messages to myself via the LINE Notify official bot.
  • Click “Generate token”

  • Select “Copy”
  • And note down the Token, if you forget it later, you will need to regenerate it, it cannot be viewed again.

githubWebhookSecret:

  • Copy & note down this random string

We will use this string as a request verification medium between Github Webhook and Google Apps Script.

Due to GAS limitations, it is not possible to obtain Headers content in doPost(e), so the standard Github Webhook verification method cannot be used, and string matching verification can only be done manually with ?secret= Query.

Create Google Apps Script

Go to Google Apps Script, click the top left corner “+ New Project”.

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

Google Apps Script

Click the top left “Untitled project” to rename the project.

Here I named the project My-Github-Repo-Notifier for easy identification in the future.

Code input area:

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
// Do not open Get method
function doGet(e) {
  return HtmlService.createHtmlOutput("Access Denied!");
}

// Github Webhook will use Post method to come in
function doPost(e) {
  const content = JSON.parse(e.postData.contents);
  
  // Security check to ensure the request is from 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!");
  }

  // Combine message
  const message = makeMessageString(content);
  
  // Send message, can also be sent to Slack, Telegram...
  sendLineNotifyMessage(message);

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

// Method
// Generate message content
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;
}

// Verify if the request is from Github Webhook
// Due to GAS limitations (https://issuetracker.google.com/issues/67764685?pli=1)
// Cannot obtain Headers content
// Therefore, the standard Github Webhook verification method (https://docs.github.com/en/webhooks-and-events/webhooks/securing-your-webhooks)
// Can only be manually matched with ?secret=XXX
function verifyGitHubWebhook(e) {
  if (e.parameter["secret"] === githubWebhookSecret) {
    return true
  } else {
    return false
  }
}

// -- Send Message --
// Line
// Other message sending methods can ask 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 carry the values copied from the previous step.

Additional Github Webhook data when someone presses Star is as follows:

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
  }
}

Deployment

After completing the program writing, click “Deploy” in the upper right corner -> “New deployment”:

On the left side, select the type “Web App”:

  • Add description: Enter anything, I entered “ Release
  • Who has access: Please change to “ Anyone
  • Click “Deploy”

For the first deployment, you need to click “Grant access”:

After the account selection pop-up appears, select your current Gmail account:

The “Google hasn’t verified this app” message appears because the app we are developing is for personal use and does not need Google verification.

Simply click “Advanced” -> “Go to XXX (unsafe)” -> “Allow”:

After deployment, you can get the Request URL in the “Web App” section of the result page. Click “Copy” and note down this GAS URL.

⚠️️️ Side note, please note that if the code is modified, you need to update the deployment for it to take effect ⚠️

To make the modified code take effect, similarly click “Deploy” in the upper right corner -> select “Manage deployments” -> select the “✏️” in the upper right corner -> version selection “Create new version” -> click “Deploy”.

This completes the code update deployment.

Github Webhook Settings

  • Go back to Github
  • We can set Webhooks for Organizations (all Repos inside) or a single Repo to listen for new ⭐️ stars

Enter Organizations / Repo -> “Settings” -> find “Webhooks” on the left -> “Add webhook”:

  • Payload URL : Enter GAS URL and manually add our own security verification string ?secret=githubWebhookSecret at the end of the URL. For example, if your GAS URL is https://script.google.com/macros/s/XXX/exec and githubWebhookSecret is 123456; then the URL is: https://script.google.com/macros/s/XXX/exec?secret=123456.
  • Content type: Select application/json
  • Which events would you like to trigger this webhook? Select “Let me select individual events.” ⚠️️ Uncheck “Pushes” ️️️️⚠️ Check “Watches”, please note it is not “Stars” (but Stars also monitor the status of clicking stars, if using Stars GAS action judgment also needs adjustment )
  • Select “Active”
  • Click “Add webhook”
  • Complete the settings

🚀 Test

Go back to the set Organizations Repo / Repo and click “Star” or un-star and then re-“Star”:

You will receive a push notification!

Done! 🎉🎉🎉🎉

Promotion

If you have any questions or comments, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here


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

Travelogue 2023 Tokyo 5-Day Free Trip

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