Skip to content

Commit

Permalink
Use GH Actions to post EOD activities to slack (#441)
Browse files Browse the repository at this point in the history
  • Loading branch information
rithviknishad authored Apr 30, 2024
1 parent 31d8a55 commit 2bc4806
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 373 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
on:
workflow_call:
inputs:
LEADERBOARD_URL:
type: string
required: true
description: URL to the Leaderboard Deployment (to access the EOD API's)

secrets:
SLACK_EOD_BOT_WEBHOOK:
GITHUB_TOKEN:
required: true

LEADERBOARD_API_KEY:
required: true

SLACK_EOD_BOT_CHANNEL:
required: true

SLACK_EOD_BOT_TOKEN:
required: true

jobs:
Expand All @@ -15,7 +30,7 @@ jobs:
with:
repository: coronasafe/leaderboard
path: ./leaderboard
sparse-checkout: scripts
sparse-checkout: activities-bot

- uses: actions/setup-node@v4
with:
Expand All @@ -27,4 +42,7 @@ jobs:
env:
GITHUB_ORG: ${{ github.repository_owner }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_EOD_BOT_WEBHOOK_URL: ${{ secrets.SLACK_EOD_BOT_WEBHOOK }}
LEADERBOARD_URL: ${{ inputs.LEADERBOARD_URL }}
LEADERBOARD_API_KEY: ${{ secrets.LEADERBOARD_API_KEY }}
SLACK_EOD_BOT_CHANNEL: ${{ secrets.SLACK_EOD_BOT_CHANNEL }}
SLACK_EOD_BOT_TOKEN: ${{ secrets.SLACK_EOD_BOT_TOKEN }}
47 changes: 47 additions & 0 deletions activities-bot/postUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const {
getContributors,
getEvents,
getEODUpdates,
postEODMessage,
mergeUpdates,
flushEODUpdates,
} = require("./utils");

async function main() {
const allContributors = await getContributors();
console.info(`⚙️ Found ${Object.keys(allContributors).length} contributors`);

console.info("⚙️ Fetching events from GitHub");
const allEvents = await getEvents(Object.keys(allContributors));

console.info("⚙️ Fetching General EOD updates");
const allEodUpdates = await getEODUpdates();

console.info("⚙️ Ready to post EOD updates onto Slack Channel");
for (const [github, slack] of Object.entries(allContributors)) {
if (github !== "rithviknishad") continue; // TODO: remove this before pushing to prod

const events = allEvents[github] ?? [];
const eodUpdates = allEodUpdates[github] ?? [];

const activityCount = events.length + eodUpdates.length;
if (activityCount === 0) {
console.info(`- ⏭️ ${github}: Skipping due to no activity.`);
continue;
}

await postEODMessage({
github,
slack,
updates: mergeUpdates(events, eodUpdates),
});
console.info(`- ✅ ${github}: Posted ${activityCount} updates.`);
}

// console.info("Flushing EOD updates from cache.");
// await flushEODUpdates();

console.info("✅ Completed!");
}

main();
259 changes: 259 additions & 0 deletions activities-bot/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
const { readFile, readdir } = require("fs/promises");
const { join } = require("path");
const matter = require("gray-matter");

const { Octokit } = require("@octokit/action");
// const { Octokit } = require("octokit");

const root = join(process.cwd(), "data-repo/contributors");

async function getContributorBySlug(file) {
const { data } = matter(await readFile(join(root, file), "utf8"));
return {
github: file.replace(/\.md$/, ""),
slack: data.slack,
};
}

export async function getContributors() {
const slugs = await readdir(`${root}`);
const contributors = await Promise.all(
slugs.map((path) => getContributorBySlug(path)),
);

return Object.fromEntries(
contributors
.filter((contributor) => !!contributor.slack)
.map((c) => [c.github, c.slack]),
);
}

export const GITHUB_ORG = process.env.GITHUB_ORG;
export const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
export const LEADERBOARD_API_KEY = process.env.LEADERBOARD_API_KEY;
export const LEADERBOARD_URL = process.env.LEADERBOARD_URL;
export const SLACK_EOD_BOT_TOKEN = process.env.SLACK_EOD_BOT_TOKEN;
export const SLACK_EOD_BOT_CHANNEL = process.env.SLACK_EOD_BOT_CHANNEL;

function isAllowedEvent(event) {
if (event.type === "PullRequestEvent") {
return event.payload.action === "opened";
}

if (event.type === "PullRequestReviewEvent") {
return true;
}
}

export const octokit = new Octokit({ auth: GITHUB_TOKEN });

export async function getEvents(allowedAuthors) {
const aDayAgoDate = new Date();
aDayAgoDate.setDate(aDayAgoDate.getDate() - 1);
const aDayAgo = aDayAgoDate.getTime();

const events = await octokit.paginate(
"GET /orgs/{org}/events",
{ org: GITHUB_ORG, per_page: 1000 },
(res) =>
res.data.filter(
(event) =>
allowedAuthors.includes(event.actor.login) &&
isAllowedEvent(event) &&
event.created_at &&
new Date(event.created_at).getTime() > aDayAgo,
),
);

return Object.groupBy(events, (e) => e.actor.login);
}

export function mergeUpdates(events, eodUpdates) {
const updates = [];
const counts = {
eod_updates: eodUpdates.length,
pull_requests: 0,
reviews: 0,
};

updates.push(...eodUpdates.map((title) => ({ title })));

for (const event of events) {
if (event.type === "PullRequestReviewEvent") {
const url = event.payload.pull_request.html_url;
if (!updates.find((a) => a.url === url)) {
counts.reviews += 1;
updates.push({
title: `Reviewed PR: "${event.payload.pull_request.title}"`,
url,
});
}
}

if (
event.type === "PullRequestEvent" &&
event.payload.action === "opened"
) {
counts.pull_requests += 1;
updates.push({
title: `Opened PR: "${event.payload.pull_request.title}"`,
url: event.payload.pull_request.html_url,
});
}
}

return { updates, counts };
}

const leaderboardApiHeaders = {
"Content-Type": "application/json",
Authorization: `${LEADERBOARD_API_KEY}`,
};

export async function getEODUpdates() {
const res = await fetch(LEADERBOARD_URL, {
headers: leaderboardApiHeaders,
});
return res.json();
}

export async function flushEODUpdates() {
const res = await fetch(LEADERBOARD_URL, {
headers: leaderboardApiHeaders,
method: "DELETE",
});
return res.json();
}

const slackApiHeaders = {
"Content-Type": "application/json",
Authorization: `Bearer ${SLACK_EOD_BOT_TOKEN}`,
};

async function sendSlackMessage(channel, text, blocks) {
const res = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: slackApiHeaders,
body: JSON.stringify({
// channel,
channel: "U02TDGQQPMJ", // TODO: replace with channel before pushign
text,
...blocks,
}),
});

const data = await res.json();
if (!data.ok) {
console.error(data);
}
}

export function getHumanReadableUpdates(
{ updates, counts },
slackID,
githubId,
) {
const colorRange = [
{ color: "#00FF00", min: 5 },
{ color: "#FFFF00", min: 1 },
{ color: "#FF0000", min: 0 },
];

const activityCount =
counts.pull_requests + counts.reviews + counts.eod_updates;

const color =
colorRange.find((range) => range.min <= activityCount)?.color || "#0000FF";

return {
attachments: [
{
color,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `
*EOD Updates for <@${slackID}>*
Summary: Opened *${counts.pull_requests}* pull requests, Reviewed *${counts.reviews}* PRs and *${counts.eod_updates}* other general updates.
<https://github.com/${githubId}|GitHub Profile> | <${process.env.NEXT_PUBLIC_META_URL}/contributors/${githubId}|Contributor Profile>
`,
},
accessory: {
type: "image",
image_url: `https://avatars.githubusercontent.com/${githubId}?s=128`,
alt_text: "profile image",
},
},

{
type: "divider",
},
{
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [
{
type: "text",
text: "Updates:",
style: { bold: true },
},
{
type: "text",
text: `\n${updates.length === 0 ? "No updates for today" : ""}`,
},
],
},
{
type: "rich_text_list",
style: "bullet",
elements: updates.map((item) => {
const elements = [
{
type: "text",
text: item.title,
},
];

if (item.url) {
let preview = "";

if (item.url.startsWith("https://github.com")) {
preview = item.url.replace("https://github.com/", "");
const [org, repo, type, number] = preview.split("/");
preview = `${repo}#${number.split("#")[0]}`;
}

elements.push({
type: "link",
text: ` ${preview} ↗`,
url: item.url,
});
}

return {
type: "rich_text_section",
elements,
};
}),
},
],
},
],
},
],
};
}

export async function postEODMessage({ github, slack, updates }) {
await sendSlackMessage(
SLACK_EOD_BOT_CHANNEL,
"",
getHumanReadableUpdates(updates, slack, github),
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { NextRequest } from "next/server";
import { EODUpdatesManager, sendSlackMessage } from "@/lib/slackbotutils";
import { getContributorBySlug } from "@/lib/api";

export const maxDuration = 300;

export async function GET(
req: NextRequest,
req: Request,
{ params }: { params: { username: string } },
) {
const { username } = params;
Expand Down
Loading

0 comments on commit 2bc4806

Please sign in to comment.