Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Sends a notification to a Slack webhook highlighting open pull requests for a gi
| GITHUB_EXCLUDED_LABELS | The labels that pull requests must not have assigned to them. Any label must be present for the merge request to be ignored. This should be comma separated. | `excluded-label-1,excluded-label-2` |
| SLACK_WEBHOOK_URL | The URL of the slack incoming webhook to send the notification to. | |
| SLACK_TARGET | The target of the slack message. This is `@here` by default. | `@here` |
| MS_TEAMS_WEBHOOK_URL | The URL of the MS Teams incoming webhook to send the notification to. | |

## Docker

Expand All @@ -29,7 +30,7 @@ Because the logic is within a docker image, it can be run in a variety of places

This is available as a GitHub Action. You can run it on a schedule like so

```
```yaml
name: Pull Request Reminder
on:
schedule:
Expand All @@ -48,8 +49,11 @@ jobs:
include-draft: 'true'
mandatory-labels: 'test-label-1,test-label-2'
excluded-labels: 'excluded-label-1,excluded-label-1'
# If sending the notification to slack
slack-webhook-url: ${{ secrets.TEST_SLACK_WEBHOOK_URL }}
slack-target: '@here'
# If sending the notification to MS Teams
ms-teams-webhook-url: ${{ secrets.TEST_MS_TEAMS_WEBHOOK_URL }}
```

## Build
Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ inputs:
required: false
slack-webhook-url:
description: 'The URL of the slack incoming webhook to send the notification to.'
required: true
required: false
slack-target:
description: 'The target of the slack message. This is `@here` by default.'
required: false
ms-teams-webhook-url:
description: 'The URL of the MS Teams incoming webhook to send the notification to.'
required: false
runs:
using: 'docker'
image: './reminder/Dockerfile'
Expand All @@ -42,3 +45,4 @@ runs:
GITHUB_EXCLUDED_LABELS: ${{ inputs.excluded-labels }}
SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }}
SLACK_TARGET: ${{ inputs.slack-target }}
MS_TEAMS_WEBHOOK_URL: ${{ inputs.ms-teams-webhook-url }}
36 changes: 29 additions & 7 deletions reminder/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GitHubService } from "./githubService";
import { MsTeamsService } from "./notifiers/msTeamsService";
import { SlackService } from "./notifiers/slackService";
import { IReminderRequest, ReminderService } from "./reminderService";

Expand All @@ -13,10 +14,15 @@ async function run() {
includeDraft: process.env.INCLUDE_DRAFT?.trim() !== "false",
mandatoryLabels: mandatoryLabelsString.length > 0 ? mandatoryLabelsString.split(',') : [],
excludedLabels: excludedLabelsString.length > 0 ? excludedLabelsString.split(',') : [],
slack: {
slack: process.env.SLACK_WEBHOOK_URL || process.env.SLACK_TARGET ?
{
webhookUrl: process.env.SLACK_WEBHOOK_URL?.trim() as string,
target: process.env.SLACK_TARGET?.trim() as string || '@here'
}
} : undefined,
msTeams: process.env.MS_TEAMS_WEBHOOK_URL ?
{
webhookUrl: process.env.MS_TEAMS_WEBHOOK_URL?.trim() as string,
} : undefined,
};

if (!config.githubAccessToken) {
Expand All @@ -27,15 +33,31 @@ async function run() {
throw new Error(`INCLUDE_WIP was not specified`);
} else if (config.includeDraft == null) {
throw new Error(`INCLUDE_DRAFT was not specified`);
} else if (!config.slack.webhookUrl) {
throw new Error(`SLACK_WEBHOOK_URL was not specified`);
} else if (!config.slack.target) {
throw new Error(`SLACK_TARGET was not specified`);
}

if (config.slack)
{
if (!config.slack.webhookUrl) {
throw new Error(`SLACK_WEBHOOK_URL was not specified`);
} else if (!config.slack.target) {
throw new Error(`SLACK_TARGET was not specified`);
}
}
else if (config.msTeams)
{
if (!config.msTeams.webhookUrl) {
throw new Error(`MS_TEAMS_WEBHOOK_URL was not specified`);
}
}
else
{
throw new Error('Either Slack or MS Teams configuration must be specified');
}

const slackService = new SlackService();
const msTeamsService = new MsTeamsService();
const gitlabService = new GitHubService();
const reminderService = new ReminderService(gitlabService, slackService);
const reminderService = new ReminderService(gitlabService, slackService, msTeamsService);

return reminderService.sendReminder(config);
}
Expand Down
98 changes: 98 additions & 0 deletions reminder/src/notifiers/msTeamsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { IGitHubMergeRequest } from "../githubService";
import { formatDistance } from 'date-fns';
import axios, { AxiosRequestConfig } from "axios";

export interface IMsTeamsConfig {
webhookUrl: string;
}

export class MsTeamsService {
async sendReminder(config: IMsTeamsConfig, mergeRequests: IGitHubMergeRequest[]) {
const body = [];
body.push(
{
"type": "TextBlock",
"text": "Merge Requests",
"style": "heading"
},
{
"type": "TextBlock",
"text": "Looks like there are some open merge requests...",
"wrap": true
}
);

const groupedMergeRequests = mergeRequests.reduce(function(rv: Record<string, IGitHubMergeRequest[]>, x: IGitHubMergeRequest) {
(rv[x.head.repo.full_name] = rv[x.head.repo.full_name] || []).push(x);
return rv;
}, {});

const groupedKeys: string[] = Object.keys(groupedMergeRequests);
for (const key of groupedKeys) {
body.push({
"type": "TextBlock",
"text": key,
"wrap": true,
"weight": "Bolder"
},);

for (const request of groupedMergeRequests[key]) {
body.push({
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": 60,
"items": [
{
"type": "TextBlock",
"text": request.title,
"wrap": true
}
]
},
{
"type": "Column",
"width": 40,
"items": [
{
"type": "TextBlock",
"text": `Opened ${formatDistance(request.created_at, new Date())} ago by ${request.user.login}`,
"wrap": true
}
]
}
],
"selectAction": {
"type": "Action.OpenUrl",
"url": request.html_url
}
});
}
}

const requestConfig: AxiosRequestConfig = {
validateStatus: () => true, // Make sure we always return
};

await axios.post(
config.webhookUrl,
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"contentUrl": null,
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": body
}
}
]
},
requestConfig
);
}
}
14 changes: 11 additions & 3 deletions reminder/src/reminderService.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { IGitHubMergeRequestRequest, GitHubService } from "./githubService";
import { IMsTeamsConfig, MsTeamsService } from "./notifiers/msTeamsService";
import { ISlackConfig, SlackService } from "./notifiers/slackService";

export interface IReminderRequest extends IGitHubMergeRequestRequest {
githubAccessToken: string;
slack: ISlackConfig;
slack?: ISlackConfig;
msTeams?: IMsTeamsConfig;
}

export class ReminderService {
constructor(private gitlabService: GitHubService, private slackService: SlackService) {}
constructor(private gitlabService: GitHubService, private slackService: SlackService, private msTeamsService: MsTeamsService) {}

async sendReminder(request: IReminderRequest) {
const mergeRequests = await this.gitlabService.getMergeRequests(request.githubAccessToken, request);

if (mergeRequests.length > 0) {
await this.slackService.sendReminder(request.slack, mergeRequests);
if (request.slack) {
await this.slackService.sendReminder(request.slack, mergeRequests);
} else if (request.msTeams) {
await this.msTeamsService.sendReminder(request.msTeams, mergeRequests);
} else {
throw new Error('Destination service configuration has not been set');
}
}
}
}