apps/contribution-report/app.ts (148 lines of code) (raw):
// Imports
import { Octokit } from "octokit";
import { DateTime, Duration } from "luxon";
import fetch from "node-fetch";
const fs = require('fs');
require('dotenv').config();
// Octokit.js
// https://github.com/octokit/core.js#readme
const octokit = new Octokit({
auth: process.env["GITHUB_SECRET"]
});
// Import configuration
let configData = fs.readFileSync('app_config.json');
let config = JSON.parse(configData);
const team_members = config.team_members
const repositories = config.repositories;
const date_start = config.date_start === null ?
DateTime.now() : DateTime.fromISO(config.date_start);
const date_end = config.date_end === null ?
DateTime.now() : DateTime.fromISO(config.date_end);
// Driver script
async function driver() {
console.log("Working.")
console.log("Have a Thai Iced Tea while you wait🍻...");
const executiveSummary = await collectExecutiveSummary();
console.log(executiveSummary);
// Write to file
let data = JSON.stringify(executiveSummary, null, 4);
const outputFile = `./executive_summary_${DateTime.now().toISO()}.json`;
fs.writeFileSync(outputFile, data);
console.log(`Done. ✨✨ Output results to ${outputFile}`);
}
// Run the app
driver();
// Collection script
async function collectExecutiveSummary() {
const teamStats = await Promise.all(team_members.map(async member => {
const repositoryStats = await Promise.all(repositories.map(async ({owner, repo}) => {
const issuesAndPrsAll = await octokit.request('GET /repos/{owner}/{repo}/issues', {
owner: owner,
repo: repo,
creator: member.github,
state: "all"
});
const issuesAndPrs = issuesAndPrsAll.data.filter(d =>
DateTime.fromISO(d.created_at) < date_end &&
DateTime.fromISO(d.created_at) > date_start);
// Get number of issues and prs
const issues = issuesAndPrs.filter(d => d.node_id.split("_")[0] === "I");
const prs = issuesAndPrs.filter(d => d.node_id.split("_")[0] === "PR");
const sumIssues = issues.length;
const sumPrs = prs.length;
const issueLinks = issues.map(i => ({
title: i.title,
date: DateTime.fromISO(i.updated_at).toLocaleString(DateTime.DATETIME_MED),
link: i.html_url,
}));
const prLinks = prs.map(p => ({
title: p.title,
status: p.state,
date: DateTime.fromISO(p.updated_at).toLocaleString(DateTime.DATETIME_MED),
link: p.html_url,
}));
// Get number of commits merged
const commits = await Promise.all(prs.map(async (pr) => {
// Get number of commits
const patch = (await fetch(pr.pull_request.patch_url));
const text = await patch.text();
const numCommits = parseInt((text.split("Subject: [PATCH 1/")[1]?.split("]")[0] ?? "1"));
// Check if commits were merged
const timeline = await (await octokit.request(`GET ${pr.events_url}`)).data;
const mergeEvents = timeline.filter(t => t.event === "merged");
if (mergeEvents.length === 0) {
return { commits: 0 };
}
return {
pr_title: pr.title,
pr_url: pr.html_url,
is_merged: mergeEvents.length !== 0,
commits: numCommits,
};
}));
const sumCommits = commits.reduce((a,b)=> {
return a + b.commits;
}, 0);
// Get Reviewed PRs
const crPrsQuery = `is:pr reviewed-by:${member.github} repo:${owner}/${repo} created:${date_start.toISODate()}..${date_end.toISODate()}`;
const crPrs = await searchRateLimiter(crPrsQuery);
const crPrLinks = crPrs.data.items.map(cp => cp.html_url);
const crCount = crPrs.data.total_count;
return {
repo: `${owner}/${repo}`,
count_review: crCount,
count_commit: sumCommits,
count_issue: sumIssues,
count_pr: sumPrs,
issues: issueLinks,
prs: prLinks,
reviews: crPrLinks,
commits: commits,
}
}));
return {
team_member: member,
repository_stats: repositoryStats,
executive_summary_member: repositoryStats.reduce((a,b) => ({
total_review: a.total_review + b.count_review,
total_commit: a.total_commit + b.count_commit,
total_issue: a.total_issue + b.count_issue,
total_pr: a.total_pr + b.count_pr,
}),
{
total_review: 0,
total_commit: 0,
total_issue: 0,
total_pr: 0,
})
}
}));
return {
date_start: date_start.toLocaleString(DateTime.DATETIME_MED),
date_end: date_end.toLocaleString(DateTime.DATETIME_MED),
executive_summary_team:
{
...teamStats.reduce((a,b) => ({
total_review: a.total_review + b.executive_summary_member.total_review,
total_commit: a.total_commit + b.executive_summary_member.total_commit,
total_issue: a.total_issue + b.executive_summary_member.total_issue,
total_pr: a.total_pr + b.executive_summary_member.total_pr,
}),
{
total_review: 0,
total_commit: 0,
total_issue: 0,
total_pr: 0,
}),
executive_summary_members: teamStats.map(t => ({
...t.executive_summary_member,
member: t.team_member.name
}))
},
team_stats: teamStats,
}
}
// Helper methods
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const searchRateLimit = 20; // per minute
let searchRateLimiterQuerys: DateTime[] = [];
async function searchRateLimiter(q: string) {
// Clear queries that are no longer limiting api
const timeNow = DateTime.now();
searchRateLimiterQuerys = searchRateLimiterQuerys.filter(d => d > (timeNow.minus(Duration.fromMillis(60 * 1000))));
// Retry later
if (searchRateLimiterQuerys.length >= searchRateLimit) {
await sleep(Math.floor(Math.random() * 60 * 1000)); // try again after waiting a random time between 0 and 1 minutes
return searchRateLimiter(q);
}
// Put in the limiter list
searchRateLimiterQuerys.push(DateTime.now())
return await octokit.request('GET /search/issues', {
q: q
});
}