Post

Google Apps Script|3-Step Guide to Build Free Github Repo Star Notifier

Developers seeking real-time Github star notifications can use Google Apps Script to connect Github Webhooks and forward star alerts directly to Line, streamlining monitoring without extra costs or tools.

Google Apps Script|3-Step Guide to Build Free Github Repo Star Notifier

点击这里查看本文章简体中文版本。

點擊這裡查看本文章正體中文版本。

This post was translated with AI assistance — let me know if anything sounds off!


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

Writing GAS to Connect Github Webhook and Forward Star Like Notifications to Line

Preface

As an open-source project maintainer, not for money or fame, but for a sense of vanity; every time I see a new ⭐️ star, I feel secretly delighted; spending time and effort on a project that people actually use and that truly helps 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 watching ⭐️ stars and often check Github to see if the ⭐️ star count has increased. I wondered if there was a more proactive way to get notified immediately when someone clicks the ⭐️ star, without having to manually track or check.

Existing Tools

First, consider finding existing tools to achieve this. I searched on Github Marketplace and found several well-made tools by experts that can be used.

Tried a few of them, but the results were disappointing. Some no longer work, some only send notifications every 5/10/20 ⭐️ stars (I’m just a small user, so even 1 new ⭐️ makes me happy 😝), and notifications can only be sent via email, but I want to use SNS notifications.

Also, installing an app just for “vanity” feels uneasy, as there are concerns about potential security risks.

On iOS, the Github app and third-party apps like GitTrends do not support this feature.

Build 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.

2024/10/12 Update

⚠️⚠️⚠️

Since Line Notify will be shut down on 2025/04/01, please refer to my latest article “Quickly migrate Line Notify to Telegram Bot notifications in 10 minutes to switch to Telegram notification integration.

Preparation Work

This article uses Line as the notification medium. If you want to use other messaging apps for notifications, 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 down to find the “Generate access token (For developers)” section.

  • Click “Generate token”

  • Token Name: Enter the desired bot title to display before the message (e.g. Github Repo Notifier: XXXX)

  • Choose where to send the message: I selected 1-on-1 chat with LINE Notify to send the message to myself via the official LINE Notify bot.

  • Click “Generate token”

  • Select “Copy”

  • And record the Token, as it cannot be viewed again and must be regenerated if forgotten later.

githubWebhookSecret

  • Copy & Note down this random string

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

Due to GAS limitations, it is not possible to get Headers content in doPost(e). Therefore, the standard Github Webhook authentication method cannot be used, and string matching verification must be done manually using the ?secret= query.

Create Google Apps Script

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

[**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 your line notify bot token: https://notify-bot.line.me/my/
const githubWebhookSecret = "XXXXX";
// Generate your 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 method is not allowed
function doGet(e) {
  return HtmlService.createHtmlOutput("Access Denied!");
}

// Github Webhook uses POST method
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!");
  }

  // Compose message
  const message = makeMessageString(content);
  
  // Send message, can be changed 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)
// Unable to get Headers content
// Therefore cannot use Github Webhook standard verification method (https://docs.github.com/en/webhooks-and-events/webhooks/securing-your-webhooks)
// Can only manually match using ?secret=XXX
function verifyGitHubWebhook(e) {
  if (e.parameter["secret"] === githubWebhookSecret) {
    return true
  } else {
    return false
  }
}

// -- Send Message --
// Line
// For other message sending methods, 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 with the values copied from the previous step.

Additional Github Webhook data received when someone presses 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
  }
}

Deployment

After completing the programming, click the top right corner “Deploy” -> “New Deployment”:

Select “Web Application” for the type on the left side:

  • Additional Instruction: Enter anything freely, I input “Release

  • Who can access: Please change to “Everyone

  • Click “Deploy”

For the first deployment, click “Grant Access”:

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

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

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

After completing the deployment, you can find the Request URL on the “Web App” section of the results page. Click “Copy” and save this GAS URL.

⚠️️️ Off-topic, please note that code changes require deployment to take effect ⚠️

To apply the code changes, click the top right “Deploy” -> select “Manage Deployments” -> click the top right “✏️” -> choose “Create New Version” -> click “Deploy”.

The code update and deployment can be completed immediately.

Github Webhook Settings

  • Back to Github

  • We can set Webhooks for Organizations (all Repos inside) or a single Repo to monitor new ⭐️ stars.

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

  • Payload URL : Enter the GAS URL and manually add your 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 will be: 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 (Note: this is not Stars — though Stars also monitor star clicks, if using Stars in GAS actions, adjustments are needed)

  • Select “Active

  • Click “Add webhook”

  • Setup Complete

🚀Test

Go back to the Organizations Repo / Repo in Settings and click “Star” or un-star first, then star again:

You will receive a push notification!

Work done! 🎉🎉🎉🎉

Business Information

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


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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