ZhgChg.Li

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 article was AI-translated — please let me know if anything looks 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

Introduction

As an open-source project maintainer, not for money or fame, but for a sense of vanity; every time a new ⭐️ star appears, I secretly rejoice; the time and effort spent on the project is truly being used and helping friends with the same problems.

Star History Chart

Star History Chart

Therefore, I have a bit of an obsession with watching the ⭐️ stars and frequently refresh 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 check.

Existing Tools

First, consider finding existing tools. I searched at Github Marketplace and found several well-made tools available.

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

Also, if the app is built just for “vanity,” it feels uneasy and there is concern about potential security risks.

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

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 shutting 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 for 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 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, which will appear before the message (e.g. Github Repo Notifer: XXXX)

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

  • Click “Generate token”

  • Choose “Copy”

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

githubWebhookSecret:

  • Copy & write 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 access Headers in doPost(e). Therefore, the Github Webhook standard authentication method cannot be used. Instead, manual string matching verification with a ?secret= query parameter must be used.

Create Google Apps Script

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

**Google Apps Script**

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:

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

// Github Webhook will use 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 also send 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 limitation (https://issuetracker.google.com/issues/67764685?pli=1)
// Unable to get Headers content
// Therefore, cannot use Github Webhook standard verification (https://docs.github.com/en/webhooks-and-events/webhooks/securing-your-webhooks)
// Can only manually match verification via ?secret=XXX
function verifyGitHubWebhook(e) {
  if (e.parameter["secret"] === githubWebhookSecret) {
    return true
  } else {
    return false
  }
}

// -- Send Message --
// Line
// For other message sending methods, you 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);
}

Set lineToken and githubWebhookSecret with the values copied from the previous step.

Additional data received from Github Webhook when someone stars the repo:

{
  "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": "[email protected]: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 code, click the top right corner “Deploy” -> “New deployment”:

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

  • Additional Instructions: Enter anything you want, I entered “Release

  • Who can access: Please change to “Everyone

  • Click “Deploy”

For the first deployment, you need to 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 deployment, you can find the Request URL on the “Web App” page. Click “Copy” and save this GAS URL.

⚠️️️ Side note: Please remember to redeploy after code changes for them to take effect ⚠️

To apply the code changes, click “Deploy” at the top right -> select “Manage deployments” -> click the “✏️” icon at the top right -> choose “Create new version” -> then click “Deploy”.

The code update and deployment can be completed immediately.

Github Webhook Settings

  • Back to Github

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

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

  • Payload URL : Enter the GAS URL and manually add your own security token ?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 (but Stars also track starring actions, so if using Stars, you need to adjust the GAS action logic accordingly)

  • Select “ Active

  • Click “Add webhook”

  • Setup Complete

🚀Test

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

You will receive a push notification!

Work done! 🎉🎉🎉🎉

Improve this page
Edit on GitHub
Originally published on Medium
Read the original
Share this essay
Copy link · share to socials
ZhgChgLi
Author

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

Comments