Docs Menu

Write a Serverless GitHub Contribution Tracker

On this page

Estimated time to complete: 30 minutes

In this tutorial, you will use MongoDB Realm to build a serverless application that monitors GitHub repositories and tracks contributions.

The app that you build will:

  • Keep track of users that have contributed to your project in a MongoDB Atlas collection.
  • Leave a comment that welcomes new contributors when they open a pull request or file an issue.
  • Generate and send a weekly report that summarizes contributions to your repository.

Once you've completed this tutorial, you will know how to:

  • Write and run serverless functions that handle your app's logic.
  • Automate scheduled tasks and respond to changing data with triggers.
  • Store and access static data, like API keys, in values.
  • Connect to an external service through an API

You'll need to have the following before you can begin this tutorial:

  • A GitHub repository. The app you build will track contributions to this repository. You can create a new repository for this tutorial or use an existing repo that you have admin access to.
  • A MongoDB Atlas cluster. If you don't already have a cluster, sign up for MongoDB Atlas and create one for free.

First, you'll need to create an application in Realm. To create the app:

  1. Open your Atlas project at cloud.mongodb.com.
  2. Click Realm in the top navigation.
  3. Click Create a New App.
  4. Name the app github-tracker-tutorial.
  5. Select your cluster to link it to the new app.
  6. Click Create Realm Application.
The app creation screen with the app name set to "github-tracker-tutorial".

We'll connect the Realm App to your GitHub repository using a GitHub repository webhook. Webhooks let your app know whenever some event, like a new commit or pull request, happens in your repository. Each event runs a serverless function where you can do something in response.

For this tutorial, we'll use the GitHub REST API to send a welcome message to contributors whenever they open their first pull request or issue on a repository.

Webhooks work by sending a request about the event to a URL that your app controls. In your app, you'll need to expose a custom endpoint with a unique URL to receive and handle the webhook requests.

To create the endpoint:

  1. Click HTTPS Endpoints in the left navigation menu.
  2. Click Add An Endpoint.
  3. Name the endpoint route /greetNewContributors.
  4. Leave the HTTP method set to POST.
  5. For authorization, choose Require a Secret and use tutorial as the secret value. This requires all incoming requests to include the query parameter secret=tutorial in the request URL.
  6. Create a new function for the endpoint and name it endpoints/greetNewContributors.
  7. For now, set up a basic handler that only responds to incoming calls without doing other work. Copy the following code into the function body:

    exports = async function greetNewContributors(request, response) {
    return response
    .setStatusCode(200)
    .setBody("Successfully received a GitHub webhook event")
    }
  8. Click Save and deploy the endpoint.
Tip

Copy the endpoint URL from the configuration page. You'll need it in the next step.

Now that you've created the endpoint in your app, you need to set up a webhook in your GitHub repository that sends events to the endpoint.

To create the webhook in your repository:

  1. Open the repository's settings and select Webhook in the left navigation menu.
  2. Add a new webhook and set the Payload URL to the URL of the endpoint that you just created. The URL will look like one of the following depending on your app's deployment model:

  3. Set the webhook content type to application/json.
  4. Set the Secret to the same secret value you defined for the endpoint: tutorial.
  5. Choose to select individual events and configure the webhook to only send events for Issues and Pull requests.
  6. Click Add webhook to save the new webhook.
Tip

To confirm that the webhook succesfully calls your endpoint, check your application logs for entries with the type Endpoint. You can also check the webhook's request logs in GitHub under Recent Deliveries on the webhook's settings page. Each successful request has a green checkmark next to it.

A log entry in GitHub that shows the custom endpoint's response to a ping event. The response has a status code of 200 and the response body says "Successfully received a GitHub webhook event".

The webhook is now set up to send events from GitHub to your endpoint. However, to respond to events in the endpoint with the GitHub API you'll need an access token. This tutorial uses personal access tokens, but you could also set up a GitHub app and use that token instead.

To create a personal access token:

  1. Open your GitHub user settings (not your repository settings) and select Developer settings in the left navigation menu.
  2. Select Personal access tokens in the left navigation menu and then click Generate new token.
  3. Configure the token with a descriptive name and a reasonable expiration time. Since this is a tutorial, consider expiring the token after 7 days.
  4. Select the repo scope.
  5. Click Generate token.
  6. Copy the token somewhere secure where you can access it again. GitHub will never show you the token again after this point.

Back in your app, add a new value to hold the personal access token you just generated. You'll be able to reference the value from your endpoint without hardcoding the token into your functions.

To create the value:

  1. Click Values in the left navigation menu.
  2. Click Create New Value.
  3. Name the value GitHubAccessToken.
  4. Leave the type set to Value.
  5. Paste the personal access token into the value's content input.
  6. Click Save.

The endpoint will interact with GitHub's REST API to leave comments. You could write and send HTTP requests directly to the API using the built-in context.http client or an external library. However, in this tutorial we use GitHub's official Node.js library called Octokit that wraps the API. Once installed, you'll be able to import the library from any function in your app.

To add the Octokit library to your app:

  1. Click Functions in the left navigation menu.
  2. Select the Dependencies tab.
  3. Click Add New Dependency.
  4. Enter the package name: @octokit/request.
  5. Click Add.
  6. Wait for Realm to install the package. The installation should complete in a few seconds, but may take up to a minute.

Now that you have an access token and have installed Octokit, you can update the endpoint function to actually do something when it receives events. Specifically, the function should:

  • Parse the incoming webhook event
  • Log the contribution in MongoDB
  • Add a comment through the GitHub API
  • Send an informative response back to GitHub

To update the function:

  1. Click Functions in the left navigation menu.
  2. Click endpoints/greetNewContributors to open the endpoint function editor.
  3. Replace the basic function with the following code:

    functions/endpoints/greetNewContributors.js
    exports = async function greetNewContributors(request, response) {
    // Parse the webhook event from the incoming request.
    const event = JSON.parse(request.body.text());
    // Don't do anything unless this is a new issue or pull request
    if (event.action !== "opened") {
    return response.setStatusCode(200);
    }
    // Get data from the GitHub webhook event.
    // Based on the webhook configuration the event will be one of the following:
    // - issues: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issues
    // - pull_request: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request
    const sender = event.sender;
    const repo = event.repository;
    const contribution = event.issue || event.pull_request;
    const contribution_url = event.issue
    ? event.issue.url
    : event.pull_request.issue_url;
    const issue_number = contribution.number;
    // Record this contribution in the user's contributor document.
    // If this user hasn't contributed to the repo before, create a document for them.
    const atlas = context.services.get("mongodb-atlas");
    const contributors = atlas.db("community").collection("contributors");
    const contributor = await contributors.findOneAndUpdate(
    // Look up the user by their GitHub login
    { login: sender.login },
    // Add this issue or pull request to their list of contributions
    {
    $push: {
    contributions: {
    date: new Date(),
    type: event.issue ? "issue" : "pull_request",
    url: contribution_url,
    },
    },
    },
    // If they haven't contributed before, add them to the database
    { upsert: true, returnNewDocument: true }
    );
    // Send a welcome message to first time contributors on their issue or pull request
    const isFirstTimeContributor = contributor.contributions.length === 1;
    if (isFirstTimeContributor) {
    const octokit = require("@octokit/request");
    await octokit.request(
    "POST /repos/{owner}/{repo}/issues/{issue_number}/comments",
    {
    headers: {
    authorization: `token ${context.values.get("GitHubAccessToken")}`,
    },
    owner: repo.owner.login,
    repo: repo.name,
    issue_number: issue_number,
    body: `Hi there ${sender.login} 👋 Thanks for your first contribution!`,
    }
    );
    }
    // Configure the HTTP response sent back to GitHub
    return response
    .setStatusCode(200)
    .setHeader("Content-Type", "application/json")
    .setBody(
    isFirstTimeContributor
    ? `This is ${sender.login}'s first contribution!`
    : `${sender.login} has contributed before.`
    );
    };
  4. Click Save and deploy the endpoint.

The welcome message endpoint should now be fully set up. To test that it works correctly, open a new issue or pull request on the repository. The endpoint will add a new comment to the thread the first time you do this but won't add a welcome message on subsequent attempts.

An example of the welcome comment in GitHub added by the endpoint.

GitHub logs repository webhook requests, so you can also check the log entry in GitHub to confirm that everything is working properly. Each request log includes a response message from the endpoint.

A log entry in GitHub that shows the custom endpoint's response for a first-time contributor
Tip

If you want to reset the test, delete the document with your GitHub username from community.contributions. This lets the app "forget" that you've contributed before and it will welcome you on your next contribution.

Your app is connected to GitHub, stores information about contributions, and welcomes new contributors. Now we'll extend it to automatically analyze and generate reports for your repository.

Your app needs a way to know which repositories it should generate reports for every week. For this tutorial, we'll hardcode the list in a value.

Create a new value called GitHubProjects that contains an array of objects. Each object specifies the owner and repo name of a GitHub repository. Make sure to include an entry for your repository.

values/projects.json
[
{ "owner": "<GitHub Username>", "repo": "<Repository Name>" }
]

A report is a document that summarizes the contributions to a repository for some time period. We'll use a function to create on-demand reports for a repo.

Create a new function named generateCommunityReport and add the following code:

functions/generateCommunityReport.js
exports = async function generateCommunityReport({ owner, repo, startDate }) {
// Look up issues and pull requests that had activity
const octokit = require("@octokit/request");
const { data: issuesWithActivity } = await octokit.request(
"GET /repos/{owner}/{repo}/issues",
{
headers: {
authorization: `token ${context.values.get("GitHubAccessToken")}`,
},
owner: owner,
repo: repo,
since: startDate,
}
);
// Look up users that contributed to the repo
const atlas = context.services.get("mongodb-atlas");
const contributors = atlas.db("community").collection("contributors");
const allContributors = await contributors
.find({
contributions: {
$elemMatch: {
date: { $gt: new Date(startDate) },
owner: owner,
repo: repo,
},
},
})
.toArray();
// Keep track of users who made their first contribution
const newContributors = allContributors.filter((c) => {
new Date(c.contributions[0].date) > new Date(startDate);
});
// Return a report with the data
return {
owner,
repo,
startDate,
issuesWithActivity,
allContributors,
newContributors,
};
};

The function you just created creates a report for a repository on-demand. However, at this point nothing calls the function and the generated reports are not saved anywhere. To actually use it, we'll create a scheduled trigger that calls the function once every week and saves the generated reports in your linked cluster.

To create the trigger:

  1. Click Triggers in the left navigation menu.
  2. Click Add a Trigger.
  3. Choose Scheduled for the trigger type.
  4. Name the trigger generateAndSaveCommunityReports
  5. Choose the Advanced schedule type
  6. Enter the following cron schedule to run once a week on Monday at 5 AM UTC:

    0 5 * * 1
  7. Create a new function for the trigger and name it triggers/generateAndSaveCommunityReports.
  8. Click Add Dependency and install moment, which we use to work with dates in the function.
  9. Copy the following code into the function body:

    functions/triggers/generateAndSaveCommunityReports.js
    exports = async function generateAndSaveCommunityReports() {
    const projects = context.values.get("GitHubProjects");
    const lastMonday = getLastMonday(); // e.g. "2022-02-21T05:00:00.000Z"
    // Generate a report for every tracked repo
    const reportsForLastWeek = await Promise.all(
    // Call the `generateCommunityReport` function for each project
    projects.map(async (project) => {
    return context.functions.execute("generateCommunityReport", {
    owner: project.owner,
    repo: project.repo,
    startDate: lastMonday,
    });
    })
    );
    // Save the generated reports in Atlas
    const atlas = context.services.get("mongodb-atlas");
    const reports = atlas.db("community").collection("reports");
    return await reports.insertMany(reportsForLastWeek);
    };
    // Get an ISO string for last Monday at 5AM UTC
    function getLastMonday() {
    const moment = require("moment");
    return moment(new Date().setUTCHours(5, 0, 0, 0))
    .utc()
    .day(1 - 7)
    .toISOString();
    }
  10. Click Save.

Your app will now automatically generate and save reports every week. However, the reports won't be very useful if nobody sees them. We'll create a database trigger that listens for new reports and creates a formatted message that you can send to end users.

To set up the messages:

  1. Click Triggers in the left navigation menu.
  2. Click Add a Trigger.
  3. Leave the trigger type set to Database.
  4. Name the trigger sendCommunityReport.
  5. Add the trigger to the community.reports collection and listen for Insert events.
  6. Enable Full Document to include each new report document in the change event passed to the trigger function.
  7. Create a new function for the trigger and name it triggers/sendCommunityReport.
  8. Copy the following code into the function body:

    functions/triggers/sendCommunityReport.js
    exports = async function sendCommunityReport(changeEvent) {
    // Pull out the report document from the database change event
    const report = changeEvent.fullDocument;
    // Format values from the report to include in the message
    const projectName = `${report.owner}/${report.repo}`;
    const moment = require("moment");
    const formattedDate = moment(report.startDate).utc().format("MMMM Do, YYYY");
    const numIssuesWithActivity = report.issuesWithActivity.length;
    const numContributors = report.allContributors.length;
    const numNewContributors = report.newContributors.length;
    // Create a message string that describes the data in the report
    const message = [
    `# Community contributions to ${projectName} since ${formattedDate}`,
    `Last week, there was activity on ${numIssuesWithActivity} issues and pull requests.`,
    `We had ${numContributors} people contribute, including ${numNewContributors} first time contributors.`,
    ].join("\n");
    // For this tutorial we'll just log the message, but you could use a
    // service to send it as an email or push notification instead.
    console.log(message);
    };
  9. Click Save and deploy the trigger.

Your app is set up to automatically generate, save, and send reports every week. To make sure that everything works, you can run this report flow manually.

Open the function editor for your scheduled trigger, triggers/generateAndSaveCommunityReports, and then click the Run button. This should generate and save on-demand reports for every repo that you listed in the GitHubProjects value.

To confirm:

  1. Check community.reports for the new report documents.
  2. Check your application's database trigger logs to find the formatted message for each report

Congratulations! You've succesfully set up a serverless GitHub contribution tracker and reached the end of this tutorial.

If you want to keep developing, you can try to add some new features to the tracker. For example, you could:

  • Update the endpoint to handle more webhook event types like issue_comment or pull_request_review.
  • Update the weekly reports to include more information from the GitHub API.
  • Connect to an external service like Twilio or SendGrid to actually send the report via email or SMS instead of just logging it.

Realm includes many services that can power your application. Check out the rest of the documentation to learn more about these services and how you can use them.

We're always working to improve our docs and tutorials. If you have a suggestion or had issues with this tutorial, click Give Feedback at the bottom of this page to rate the tutorial and send us a comment.

The official MongoDB Community Forums are a great place to meet other developers, ask and answer questions, and stay up-to-date with the latest Realm features and releases.

←  Node.js CLI TutorialSet up JWT Authentication with MongoDB Realm →
Give Feedback
© 2022 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2022 MongoDB, Inc.