Top 4 ways to send notifications about new GitHub stars

Eric AllamEric Allam

Top 4 ways to send notifications about new GitHub stars

TL;DR

In the last article, I discussed creating a GitHub stars monitor. In this article, I want to show you how you can be informed about new stars daily. We will learn:

  • How to build a generic system to create and use providers.
  • How to use the providers to send notifications.
  • Different use cases of using different providers.

Notifications


Your background job platform 🔌

Trigger.dev is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

 

GiveUsStars

Please help us with a star đŸ„č. It would help us to create more articles like this 💖

Star the Trigger.dev repository ⭐


Let’s set it up đŸ”„

We are going to create different providers to inform us when there are new stars. We will set up Email, SMS, Slack, and Discord notifications.

Our goal is to make it simple enough for every contributor to contribute more providers in the future.

Each provider will have a different set of parameters, some only API keys and some phone numbers, depending on the providers.

To validate those keys, let’s install zod; it’s a great library to define schemas and check data against them.

You can start by running:


_10
npm install zod --save

Once that is done, create a new folder called providers and then a new file inside called register.provider.ts.

here is the code of the file:


_23
import {Schema} from "zod";
_23
_23
export function registerProvider<T>(
_23
name: string,
_23
options: {active: boolean},
_23
validation: Schema<T>,
_23
run: (libName: string, stars: number, values: T) => Promise<void>
_23
) {
_23
// if not active, we can just pass an empty function, nothing will run
_23
if (!options.active) {
_23
return () => {};
_23
}
_23
_23
// will validate and remove unnecessary values (Security wise)
_23
const env = validation.parse(process.env);
_23
_23
// return the function we will run at the end of the job
_23
return async (libName: string, stars: number) => {
_23
console.log(`Running provider ${name}`);
_23
await run(libName, stars, env as T);
_23
console.log(`Finished running provider ${name}`);
_23
}
_23
}

It’s not a lot of code, but it might be a little bit complex.

We are starting by creating a new function called registerProvider. That function gets a generic type T, which is basically our required environment variables.

Then we have 4 more parameters:

  • name - that can be any of Twilio, Discord, Slack, or Resend.
  • options - currently, one parameter is the provider active or not?
  • validation - here, we pass the zod schema of our required parameters in our .env file.
  • run - That actually functions to send the notifications. Pay attention that the parameters that are being passed into it are the library name, the number of stars, and the environment variables that we specified in validation

Then we have the actual function:

First, we check if the provider is active or not. If not, we send an empty function.

Then, we validate and extract the variables we specify in our schema. If the variables are missing zod will send an error and will not let the application run.

Lastly, we return a function that gets the library name and the number of stars and triggers the notification.

Inside our providers folder, create a new file called providers.ts and add the following code inside:


_10
export const Providers = [];

Later, we will add all our providers there.


Modify TriggerDev jobs

This article is a continuation of the previous article on creating a GitHub stars monitor.

Edit the file jobs/sync.stars.ts and add the following code to the bottom of the file:


_17
const triggerNotification = client.defineJob({
_17
id: "trigger-notification",
_17
name: "Trigger Notification",
_17
version: "0.0.1",
_17
trigger: invokeTrigger({
_17
schema: z.object({
_17
stars: z.number(),
_17
library: z.string(),
_17
providerNumber: z.number(),
_17
})
_17
}),
_17
run: async (payload, io, ctx) => {
_17
await io.runTask("trigger-notification", async () => {
_17
return Providers[payload.providerNumber](payload.library, payload.stars);
_17
});
_17
}
_17
});

This job gets the number of stars, library name, and provider number and triggers the notification for a specific provider from the previously defined providers.

Now, let’s go ahead and modify getStars at the end of the function add the following code:


_10
for (let i = 0; i < Providers.length; i++) {
_10
await triggerNotification.invoke(payload.name + '-' + i, {
_10
library: payload.name,
_10
stars: stargazers_count - payload.previousStarCount,
_10
providerNumber: i,
_10
});
_10
}

This will trigger a notification for every library.

The full page code:


_100
import { cronTrigger, invokeTrigger } from "@trigger.dev/sdk";
_100
import { client } from "@/trigger";
_100
import { prisma } from "../../helper/prisma";
_100
import axios from "axios";
_100
import { z } from "zod";
_100
import {Providers} from "@/providers/providers";
_100
_100
// Your first job
_100
// This Job will be triggered by an event, log a joke to the console, and then wait 5 seconds before logging the punchline.
_100
client.defineJob({
_100
id: "sync-stars",
_100
name: "Sync Stars Daily",
_100
version: "0.0.1",
_100
// Run a cron every day at 23:00 AM
_100
trigger: cronTrigger({
_100
cron: "0 23 * * *",
_100
}),
_100
run: async (payload, io, ctx) => {
_100
const repos = await io.runTask("get-stars", async () => {
_100
// get all libraries and current amount of stars
_100
return await prisma.repository.groupBy({
_100
by: ["name"],
_100
_sum: {
_100
stars: true,
_100
},
_100
});
_100
});
_100
_100
//loop through all repos and invoke the Job that gets the latest stars
_100
for (const repo of repos) {
_100
await getStars.invoke(repo.name, {
_100
name: repo.name,
_100
previousStarCount: repo?._sum?.stars || 0,
_100
});
_100
}
_100
},
_100
});
_100
_100
const getStars = client.defineJob({
_100
id: "get-latest-stars",
_100
name: "Get latest stars",
_100
version: "0.0.1",
_100
// Run a cron every day at 23:00 AM
_100
trigger: invokeTrigger({
_100
schema: z.object({
_100
name: z.string(),
_100
previousStarCount: z.number(),
_100
}),
_100
}),
_100
run: async (payload, io, ctx) => {
_100
const stargazers_count = await io.runTask("get-stars", async () => {
_100
const {data} = await axios.get(`https://api.github.com/repos/${payload.name}`, {
_100
headers: {
_100
authorization: `token ${process.env.TOKEN}`,
_100
},
_100
});
_100
return data.stargazers_count as number;
_100
});
_100
_100
await io.runTask("upsert-stars", async () => {
_100
await prisma.repository.upsert({
_100
where: {
_100
name_day_month_year: {
_100
name: payload.name, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(),
_100
},
_100
}, update: {
_100
stars: stargazers_count - payload.previousStarCount,
_100
}, create: {
_100
name: payload.name, stars: stargazers_count - payload.previousStarCount, month: new Date().getMonth() + 1, year: new Date().getFullYear(), day: new Date().getDate(),
_100
},
_100
});
_100
});
_100
_100
for (let i = 0; i < Providers.length; i++) {
_100
await triggerNotification.invoke(payload.name + '-' + i, {
_100
library: payload.name,
_100
stars: stargazers_count - payload.previousStarCount,
_100
providerNumber: i,
_100
});
_100
}
_100
},
_100
});
_100
_100
const triggerNotification = client.defineJob({
_100
id: "trigger-notification",
_100
name: "Trigger Notification",
_100
version: "0.0.1",
_100
trigger: invokeTrigger({
_100
schema: z.object({
_100
stars: z.number(),
_100
library: z.string(),
_100
providerNumber: z.number(),
_100
})
_100
}),
_100
run: async (payload, io, ctx) => {
_100
await io.runTask("trigger-notification", async () => {
_100
return Providers[payload.providerNumber](payload.library, payload.stars);
_100
});
_100
}
_100
});

Now, the fun part 🎉

Let’s go ahead and create our providers!

First create a new folder called providers/lists


1. Discord

Discord

Create a new file called discord.provider.ts and add the following code:


_14
import {object, string} from "zod";
_14
import {registerProvider} from "@/providers/register.provider";
_14
import axios from "axios";
_14
_14
export const DiscordProvider = registerProvider(
_14
"discord",
_14
{active: true},
_14
object({
_14
DISCORD_WEBHOOK_URL: string(),
_14
}),
_14
async (libName, stars, values) => {
_14
await axios.post(values.DISCORD_WEBHOOK_URL, {content: `The library ${libName} has ${stars} new stars!`});
_14
}
_14
);

As you can see, we are using the registerProvider to create a new provider called DiscordProvider

  • We set the name to discord
  • We set it to be active
  • We specify that we need an environment variable called DISCORD_WEBHOOK_URL.
  • We use a simple post command with Axios to add the information to the check.

To get DISCORD_WEBHOOK_URL:

  1. Go to your Discord server
  2. Click edit on one of the channels
  3. Go to Integrations
  4. Click Create Webhook
  5. Click on the created webhook, then click Copy webhook URL

Edit our .env file on our root project and add


_10
SLACK_WEBHOOK_URL=<your copied url>

Spidy


2. Slack

Slack

Create a new file called slack.provider.ts and add the following code:


_14
import {object, string} from "zod";
_14
import {registerProvider} from "@/providers/register.provider";
_14
import axios from "axios";
_14
_14
export const SlackProvider = registerProvider(
_14
"slack",
_14
{active: true},
_14
object({
_14
SLACK_WEBHOOK_URL: string(),
_14
}),
_14
async (libName, stars, values) => {
_14
await axios.post(values.SLACK_WEBHOOK_URL, {text: `The library ${libName} has ${stars} new stars!`});
_14
}
_14
);

As you can see, we are using the registerProvider to create a new provider called SlackProvider

  • We set the name to slack
  • We set it to be active
  • We specify that we need an environment variable called SLACK_WEBHOOK_URL.
  • We use a simple post command with Axios to add the information to the check.

To get SLACK_WEBHOOK_URL:

  1. Create a new Slack app by using this URL: https://api.slack.com/apps?new_app=1
  2. Select the first option: “From scratch”
  3. Give an app name (any) and Slack the workspace you would like to add the notifications too. Click Create App.
  4. In “Add features and functionalities,” click Incoming hook
  5. In Activate Incoming Webhooks, change it to “On”.
  6. Click on “Add New Webhook to Workspace”.
  7. Select the channel you want and click “Allow”.
  8. Copy the webhook URL.

Edit our .env file on our root project and add


_10
SLACK_WEBHOOK_URL=<your copied url>

SlackBot


3. Email

Email

You can use different kinds of email providers. For example, we will use Resend to send emails.

For that, let’s install resend on our project:


_10
npm install resend --save

Create a new file called resend.provider.ts and add the following code:


_21
import {object, string} from "zod";
_21
import {registerProvider} from "@/providers/register.provider";
_21
import axios from "axios";
_21
import { Resend } from 'resend';
_21
_21
export const ResendProvider = registerProvider(
_21
"resend",
_21
{active: true},
_21
object({
_21
RESEND_API_KEY: string(),
_21
}),
_21
async (libName, stars, values) => {
_21
const resend = new Resend(values.RESEND_API_KEY);
_21
await resend.emails.send({
_21
from: "Eric Allam <[email protected]>",
_21
_21
subject: 'New GitHub stars',
_21
html: `The library ${libName} has ${stars} new stars!`,
_21
});
_21
}
_21
);

As you can see, we are using the registerProvider to create a new provider called ResendProvider

  • We set the name to resend
  • We set it to be active
  • We specify that we need an environment variable called RESEND_API_KEY.
  • We use the Resend library to send an email to ourselves with the new number of stars.

To get RESEND_API_KEY:

  1. Create a new account at: https://resend.com
  2. Go to API Keys or use this URL https://resend.com/api-keys
  3. Click “+ Create API Key,” add the Key name, choose “Sending access” and use the default “All Domains”. Click Add.
  4. Copy the API Key.

Edit our .env file on our root project and add


_10
RESEND_API_KEY=<your API key>

Eric Allam


4. SMS

Twilio

SMS are a little bit more complex as they require multiple variables.

For that, let’s install Twilio on our project:


_10
npm install twilio --save

Create a new file called twilio.provider.ts and add the following code:


_23
import {object, string} from "zod";
_23
import {registerProvider} from "@/providers/register.provider";
_23
import axios from "axios";
_23
import client from 'twilio';
_23
_23
export const TwilioProvider = registerProvider(
_23
"twilio",
_23
{active: true},
_23
object({
_23
TWILIO_SID: string(),
_23
TWILIO_AUTH_TOKEN: string(),
_23
TWILIO_FROM_NUMBER: string(),
_23
TWILIO_TO_NUMBER: string(),
_23
}),
_23
async (libName, stars, values) => {
_23
const twilio = client(values.TWILIO_SID, values.TWILIO_AUTH_TOKEN);
_23
await twilio.messages.create({
_23
body: `The library ${libName} has ${stars} new stars!`,
_23
from: values.TWILIO_FROM_NUMBER,
_23
to: values.TWILIO_TO_NUMBER,
_23
});
_23
}
_23
);

As you can see, we are using the registerProvider to create a new provider called TwilioProvider

  • We set the name to twilio
  • We set it to be active
  • We specify that we need environment variables: TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER and TWILIO_TO_NUMBER
  • We use the Twilio create function to send an SMS.

To get TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER and TWILIO_TO_NUMBER

  1. Create a new account at https://twilio.com
  2. Mark that you want to use it to send SMSs.
  3. Click “Get a phone number”
  4. Copy the “Account SID”, “Auth Token” and “My Twilio Phone Number”

Edit our .env file on our root project and add


_10
TWILIO_SID=<your SID key>
_10
TWILIO_AUTH_TOKEN=<your AUTH TOKEN key>
_10
TWILIO_FROM_NUMBER=<your FROM number>
_10
TWILIO_TO_NUMBER=<your TO number>

TwilioSMSs


Create new providers

As you can see, now it’s super easy to create providers.

You can also use the open-source community to create new providers since they only need to create one new file inside the providers/list directory.

The last thing to do is edit your providers.ts file and add all your providers.


_11
import {DiscordProvider} from "@/providers/list/discord.provider";
_11
import {ResendProvider} from "@/providers/list/resend.provider";
_11
import {SlackProvider} from "@/providers/list/slack.provider";
_11
import {TwilioProvider} from "@/providers/list/twilio.provider";
_11
_11
export const Providers = [
_11
DiscordProvider,
_11
ResendProvider,
_11
SlackProvider,
_11
TwilioProvider,
_11
];

Feel free to create more providers for push notifications, web push notifications, in-app notifications, etc.

And you are done đŸ„ł


Let's connect! 🔌

As an open-source developer, you're invited to join our community to contribute and engage with maintainers. Don't hesitate to visit our GitHub repository to contribute and create issues related to Trigger.dev.

The source for this tutorial is available here:

https://github.com/triggerdotdev/blog/tree/main/stars-monitor-notifications

Thank you for reading!