Our engineering department shifted from Slack to Microsoft Teams a few months ago as to cut costs because of COVID-19 and get the entire company on one chat app. The transition has been a little rough around the edges and one of those pain points have been moving from an vast amount of integrations for Slack to a smaller set of integrations for Teams.
One of those pain points are Github integrations, coming from slack we would get alerts for new Pull Requests, when PRs received comments or approvals and when PRs were merged. Since our team wanted something similar and there was nothing out there I could find readily available I decided to create my own service to post to one of channels on Teams when a PR was created. So lets get started...
Two easy first steps: setup an incoming webhook integration on teams and setup an outgoing webhook integration in Github.
Setting up an Incoming Webhook on Teams
- To the right of a Channel or Team click the ellipsis
...
- In the dropdown select Connectors.
- From there Select or Search "Incoming Webhooks".
Setting up Github Webhook
- Click on Settings at the top right-hand corner of your repo.
- Then select Webhooks on the left menu.
- URL TBD...
Now to the fun stuff...
Creating a Serverless Function
The easiest way to create a function to use for our webhook is Serverless. As per their docs to install run the following:
MacOS/Linux
curl -o- -L https://slss.io/install | bash
Windows
choco install serverless
NPM
npm install -g serverless
Using the installed Serverless CLI create a new project from a template. This will guide you through create a new service.
# Create a new Serverless service/project
$ serverless
# Change into the newly created directory
$ cd your-service-name
Now lets get started with the fun stuff. We are going to look at 3 common events, Created a Pull Request (PR), commenting on a PR, and approving a PR.
- We need to determine the event and grab the payload in our a function.
// handler.js
const pullRequest = require('./pullRequest');
const events = {
pull_request: pullRequest,
};
module.exports = function webhook(e) {
const event = e.headers["X-GitHub-Event"] || e.headers["x-github-event"];
const payload = JSON.parse(e.body);
const card = events[event] && events[event](payload);
if (!card)
return {
statusCode: 200,
body: JSON.stringify({ message: `Webhook not setup for ${event}` }),
};
try {
const res = await fetch(config.teams.webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(card),
});
if (res.ok) {
return { statusCode: 200 };
} else {
return {
statusCode: res.status,
body: JSON.stringify({ message: res.statusText }),
};
}
} catch (err) {
return { statusCode: 500, body: JSON.stringify({ error: err }) };
}
};
- For simplicity we will just look at the
opened
action of apull_request
event.
const fetch = require("node-fetch");
const { utcToZonedTime, format } = require("date-fns-tz");
module.exports = async function pullRequest(payload) {
const {
title,
body,
user,
html_url,
head,
created_at,
action,
} = payload.pull_request;
const { repo } = head;
if (action !== "opened") return;
let author;
try {
const res = await fetch(user.url);
author = await res.json();
} catch (error) {
console.error(error);
}
const timeZone = "America/Denver";
const zonedTime = utcToZonedTime(new Date(createdAt), timeZone);
const dateFormat = "EEE. MMM do, yyyy 'at' h:mm:ss bbbb";
const card = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
summary: `New ${repo.name} Pull Request by ${author.name || author.login}`,
themeColor: "0078D7",
title: title,
sections: [
{
activityTitle: `Author: ${
author.name ? `${author.name} (${author.login})` : author.login
}`,
activitySubtitle: format(zonedTime, dateFormat, { timeZone }),
activityImage: user.avatar_url,
facts: [
{
name: "Repository:",
value: repo.name,
},
{
name: "Reviewers:",
value: reviewers.length
? reviewers
.map((reviewer) => `**${reviewer.name || reviewer.login}**`)
.join(", ")
: "none",
},
],
text: body,
},
],
potentialAction: [
{
"@type": "OpenUri",
name: "View in GitHub",
targets: [{ os: "default", uri: prUrl }],
},
],
};
return card;
};
- Now that we have our handler and card creation set up we can deploy the function to API Gateway and Lambda.
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
service: github-teams-webhook
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"
provider:
name: aws
runtime: nodejs12.x
region: us-east-1
functions:
teams:
handler: handler.webhook
name: ${self:service}
# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details
events:
- http:
path: webhook
method: post
cors: true
There you go your first (or not) GitHub to Microsoft Teams webhook. If you would like this intro to go deeper or want more articles on fun little serverless side projects comment below.