From ff44365fe5119cdc74fcb16fee51857e77360ca3 Mon Sep 17 00:00:00 2001
From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com>
Date: Wed, 9 Oct 2024 09:47:38 +0530
Subject: [PATCH 01/10] feat: add page to list open external pull requests
---
app.js | 6 ++++++
src/helpers.js | 32 ++++++++++++++++++++++++++++++--
src/routes.js | 25 +++++++++++++++++++++++++
views/home.html | 1 +
4 files changed, 62 insertions(+), 2 deletions(-)
diff --git a/app.js b/app.js
index ccc01ef..20176c7 100644
--- a/app.js
+++ b/app.js
@@ -233,6 +233,12 @@ http
case "POST /cla":
routes.submitCla(req, res, app);
break;
+ case "GET /contributions/sync":
+ routes.syncPullRequests(req, res, app);
+ break;
+ case "GET /contributions":
+ routes.listPullRequests(req, res, app);
+ break;
case "POST /api/webhook":
middleware(req, res);
break;
diff --git a/src/helpers.js b/src/helpers.js
index e864065..8f52310 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -1,12 +1,14 @@
import { storage } from "./storage.js";
import { resolve } from "path";
import { PROJECT_ROOT_PATH } from "./config.js";
+import url from "node:url";
export function parseUrlQueryParams(urlString) {
if(!urlString) return urlString;
try{
- const url = new URL(urlString);
- const params = new URLSearchParams(url.search);
+ const parsedUrl = url.parse(urlString)
+ const query = parsedUrl.query;
+ const params = new URLSearchParams(query);
return Object.fromEntries(params.entries());
} catch(err){
console.error(err);
@@ -270,6 +272,7 @@ export async function getOctokitForOrg(app, org) {
return octokit
}
}
+ console.error("No GitHub App installation found for " + org);
}
export async function verifyGitHubAppAuthenticationAndAccess(app) {
@@ -333,4 +336,29 @@ function parseRepoUrl(repoUrl) {
// Handle cases where URL constructor fails (e.g., SSH URLs)
return null;
}
+}
+
+export async function getOpenPullRequests(app, owner, repo) {
+ const octokit = await getOctokitForOrg(app, owner);
+ if (!octokit) {
+ console.error("Failed to search PR because of undefined octokit intance")
+ return
+ }
+ const query = `is:pr is:open -author:dependabot[bot]` + (repo ? ` repo:${owner / repo}` : ` org:${owner}`);
+ const response = await octokit.rest.search.issuesAndPullRequests({
+ q: query,
+ sort: 'created',
+ order: 'desc'
+ });
+ const humanPRs = response.data.items.filter(pr => pr.user && pr.user.type === 'User');
+ return humanPRs;
+}
+
+export async function getOpenExternalPullRequests(app, owner, repo) {
+ const openPRs = await getOpenPullRequests(app, owner, repo);
+ if (Array.isArray(openPRs)) {
+ // Send only the external PRs
+ return openPRs?.filter((pr) => isExternalContribution(pr))
+ }
+ return
}
\ No newline at end of file
diff --git a/src/routes.js b/src/routes.js
index 64af41b..ff420e4 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -9,6 +9,7 @@ import {
queryStringToJson,
parseUrlQueryParams,
jsonToCSV,
+ getOpenExternalPullRequests,
} from "./helpers.js";
import { isPasswordValid } from "./auth.js";
@@ -166,6 +167,30 @@ export const routes = {
})
},
+ syncPullRequests(req, res, app) {
+ if (err) {
+ res.writeHead(404);
+ res.write("Not implemented yet");
+ return res.end();
+ }
+ res.writeHead(302, {
+ Location: "/pr",
+ });
+ return res.end();
+ },
+
+ async listPullRequests(req, res, app) {
+ const { org, repo } = parseUrlQueryParams(req.url) || {};
+ if (!org) {
+ res.writeHead(400);
+ return res.end("Please add org parameter in the url e.g. ?org=my-github-org-name");
+ }
+ const prList = await getOpenExternalPullRequests(app, org, repo);
+ res.setHeader('Content-Type', 'application/json');
+ const jsonString = prList ? JSON.stringify(prList, null, 2) : ("No Open Pull Requests found (or you don't have access to search PRs for " + org);
+ return res.end(jsonString);
+ },
+
default(req, res) {
res.writeHead(404);
res.write("Path not found!");
diff --git a/views/home.html b/views/home.html
index 237a6a8..a65d21b 100644
--- a/views/home.html
+++ b/views/home.html
@@ -30,6 +30,7 @@
🙏 Get started with contribution to RudderStack Open Source
Tools & resources for RudderStack contributors
+ - Contributions in progress
- Contribution guide
- Create a new integration
- Request a new feature
From 97a0b77e549e27e411dd38de7a37c64485907277 Mon Sep 17 00:00:00 2001
From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com>
Date: Wed, 9 Oct 2024 15:36:35 +0530
Subject: [PATCH 02/10] feat: group pr results by user and repo
---
src/helpers.js | 64 ++++++++++++++++++++++++----
src/routes.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 162 insertions(+), 14 deletions(-)
diff --git a/src/helpers.js b/src/helpers.js
index 8f52310..8733e30 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -60,10 +60,14 @@ export function isMessageAfterMergeRequired(pullRequest) {
}
export function isExternalContribution(pullRequest) {
- if (
- pullRequest?.head?.repo?.full_name !== pullRequest?.base?.repo?.full_name
- ) {
+ if (pullRequest?.head?.repo?.full_name !== pullRequest?.base?.repo?.full_name) {
return true;
+ } else if (pullRequest?.author_association?.toUpperCase() === 'NONE') {
+ // They have neither been the owner, member, collborater, nor they have contributed in the past
+ return true
+ } else if (pullRequest?.author_association?.toUpperCase() === 'CONTRIBUTOR') {
+ // They have contributed in the past (at least in the past as contributor)
+ return true
}
return false;
}
@@ -231,6 +235,7 @@ export function getMessage(name, context) {
}
export function isCLASigned(username) {
+ if (!username) return
const userData = storage.get({ username: username, terms: "on" });
if (userData?.length > 0) {
return true;
@@ -344,21 +349,62 @@ export async function getOpenPullRequests(app, owner, repo) {
console.error("Failed to search PR because of undefined octokit intance")
return
}
- const query = `is:pr is:open -author:dependabot[bot]` + (repo ? ` repo:${owner / repo}` : ` org:${owner}`);
+ const query = `is:pr is:open -author:dependabot[bot]` + (repo ? ` repo:${owner + "/" + repo}` : ` org:${owner}`);
const response = await octokit.rest.search.issuesAndPullRequests({
q: query,
sort: 'created',
order: 'desc'
});
+ console.log(response?.data.total_count + " results found for search: " + query);
const humanPRs = response.data.items.filter(pr => pr.user && pr.user.type === 'User');
return humanPRs;
}
export async function getOpenExternalPullRequests(app, owner, repo) {
- const openPRs = await getOpenPullRequests(app, owner, repo);
- if (Array.isArray(openPRs)) {
+ try {
+ const openPRs = await getOpenPullRequests(app, owner, repo);
+ if (!Array.isArray(openPRs)) {
+ return;
+ }
// Send only the external PRs
- return openPRs?.filter((pr) => isExternalContribution(pr))
+ const openExternalPRs = openPRs?.filter((pr) => isExternalContribution(pr))
+ return openExternalPRs
+ } catch (err) {
+ return
}
- return
-}
\ No newline at end of file
+}
+
+export function timeAgo(date) {
+ if (!date) return '';
+ if (typeof date === 'string') {
+ date = new Date(date);
+ }
+ const now = new Date();
+ const seconds = Math.floor((now - date) / 1000);
+ let interval = Math.floor(seconds / 31536000);
+
+ if (interval > 1) {
+ return `${interval} years ago`;
+ }
+ interval = Math.floor(seconds / 2592000);
+ if (interval > 1) {
+ return `${interval} months ago`;
+ }
+ interval = Math.floor(seconds / 604800);
+ if (interval > 1) {
+ return `${interval} weeks ago`;
+ }
+ interval = Math.floor(seconds / 86400);
+ if (interval > 1) {
+ return `${interval} days ago`;
+ }
+ interval = Math.floor(seconds / 3600);
+ if (interval > 1) {
+ return `${interval} hours ago`;
+ }
+ interval = Math.floor(seconds / 60);
+ if (interval > 1) {
+ return `${interval} minutes ago`;
+ }
+ return `${seconds} seconds ago`;
+}
diff --git a/src/routes.js b/src/routes.js
index ff420e4..8dfc1a3 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -5,11 +5,13 @@ import { PROJECT_ROOT_PATH } from "./config.js";
import { storage } from "./storage.js";
import { sanitizeInput } from "./sanitize.js";
import {
+ isCLASigned,
afterCLA,
queryStringToJson,
parseUrlQueryParams,
jsonToCSV,
getOpenExternalPullRequests,
+ timeAgo
} from "./helpers.js";
import { isPasswordValid } from "./auth.js";
@@ -185,15 +187,115 @@ export const routes = {
res.writeHead(400);
return res.end("Please add org parameter in the url e.g. ?org=my-github-org-name");
}
- const prList = await getOpenExternalPullRequests(app, org, repo);
- res.setHeader('Content-Type', 'application/json');
- const jsonString = prList ? JSON.stringify(prList, null, 2) : ("No Open Pull Requests found (or you don't have access to search PRs for " + org);
- return res.end(jsonString);
+ const prs = await getOpenExternalPullRequests(app, org, repo);
+ if (req.headers['content-type']?.toLowerCase() === 'application/json') {
+ res.setHeader('Content-Type', 'application/json');
+ const jsonString = prs ? JSON.stringify(prs, null, 2) : ("No Open Pull Requests found (or you don't have access to search PRs for " + org);
+ return res.end(jsonString);
+ }
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.write(`
+
+
+ Recent Contributions (Open)
+
+
+
+ Recent Contributions (Open)
+
+ ${Array.isArray(prs) && prs.length > 0 ? `` : ""}
+
+
+ ${groupPullRequestsByUser(prs)}
+
+
+ ${groupPullRequestsByRepo(prs)}
+
+
+
+ `);
+ res.end();
},
-
+ // ${!Array.isArray(prs) || prs?.length < 1 ? "No contributions found! (Might be an access issue)" : prs?.map(pr => `- ${pr?.user?.login} contributed a PR - ${pr?.title} [${pr?.labels?.map(label => label?.name).join('] [')}] updated ${timeAgo(pr?.updated_at)}
`).join('')}
default(req, res) {
res.writeHead(404);
res.write("Path not found!");
return res.end();
},
};
+
+
+function groupPullRequestsByUser(prs) {
+ if (!Array.isArray(prs) || prs?.length < 1) {
+ return "No recent contributions found"
+ }
+ const grouped = prs?.reduce((acc, pr) => {
+ if (!acc[pr?.user?.login]) {
+ acc[pr?.user?.login] = [];
+ }
+ acc[pr?.user?.login].push(pr);
+ return acc;
+ }, {});
+ let html = '';
+ for (const user in grouped) {
+ html += `${user}
${isCLASigned(user) ? "✅" : ""}`;
+ grouped[user].forEach(pr => {
+ html += `
+ -
+ ${pr?.title}
+ [${pr?.labels?.map(label => label?.name).join('] [')}]
+ updated ${timeAgo(pr?.updated_at)}
+
`;
+ });
+ html += '
';
+ }
+ return html;
+}
+
+function groupPullRequestsByRepo(prs) {
+ if (!Array.isArray(prs) || prs?.length < 1) {
+ return "No recent contributions found"
+ }
+ const grouped = prs?.reduce((acc, pr) => {
+ if (!acc[pr?.repository_url]) {
+ acc[pr?.repository_url] = [];
+ }
+ acc[pr?.repository_url].push(pr);
+ return acc;
+ }, {});
+ let html = '';
+ for (const repo in grouped) {
+ const repoName = repo.split('/').slice(-1)[0];
+ html += `${repoName}
';
+ }
+ return html;
+}
\ No newline at end of file
From 54741e3df9d98a12df073ec9471d559f868c4e47 Mon Sep 17 00:00:00 2001
From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com>
Date: Sat, 12 Oct 2024 08:12:51 +0530
Subject: [PATCH 03/10] feat: use default gh org when no installation found in
target org
---
src/helpers.js | 24 ++++++++++++++-----
src/routes.js | 64 ++++++++++++++++++++++++++++++--------------------
2 files changed, 56 insertions(+), 32 deletions(-)
diff --git a/src/helpers.js b/src/helpers.js
index 8733e30..ec1dd6f 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -278,6 +278,11 @@ export async function getOctokitForOrg(app, org) {
}
}
console.error("No GitHub App installation found for " + org);
+ // Fall back authentication method
+ const DEFAULT_GITHUB_ORG = process.env.DEFAULT_GITHUB_ORG;
+ if (DEFAULT_GITHUB_ORG && org !== DEFAULT_GITHUB_ORG) {
+ return await getOctokitForOrg(app, DEFAULT_GITHUB_ORG);
+ }
}
export async function verifyGitHubAppAuthenticationAndAccess(app) {
@@ -343,26 +348,33 @@ function parseRepoUrl(repoUrl) {
}
}
-export async function getOpenPullRequests(app, owner, repo) {
+export async function getOpenPullRequests(app, owner, repo, options) {
const octokit = await getOctokitForOrg(app, owner);
if (!octokit) {
console.error("Failed to search PR because of undefined octokit intance")
return
}
- const query = `is:pr is:open -author:dependabot[bot]` + (repo ? ` repo:${owner + "/" + repo}` : ` org:${owner}`);
+ let query = `is:pr is:open` + (repo ? ` repo:${owner + "/" + repo}` : ` org:${owner}`);
+ const BOT_USERS = process.env.GITHUB_BOT_USERS ? process.env.GITHUB_BOT_USERS.split(",")?.map((item) => item?.trim()) : null;
+ const GITHUB_ORG_MEMBERS = process.env.GITHUB_ORG_MEMBERS ? process.env.GITHUB_ORG_MEMBERS.split(",")?.map((item) => item?.trim()) : null;
+ // Remove results from bots or internal team members
+ BOT_USERS?.forEach((botUser) => query += (" -author:" + botUser));
+ GITHUB_ORG_MEMBERS?.forEach((orgMember) => query += (" -author:" + orgMember));
const response = await octokit.rest.search.issuesAndPullRequests({
q: query,
+ per_page: 100,
+ page: options?.page || 1,
sort: 'created',
order: 'desc'
});
- console.log(response?.data.total_count + " results found for search: " + query);
- const humanPRs = response.data.items.filter(pr => pr.user && pr.user.type === 'User');
+ console.log(response?.data?.total_count + " results found for search: " + query);
+ const humanPRs = response?.data?.items?.filter(pr => pr.user && pr.user.type === 'User');
return humanPRs;
}
-export async function getOpenExternalPullRequests(app, owner, repo) {
+export async function getOpenExternalPullRequests(app, owner, repo, options) {
try {
- const openPRs = await getOpenPullRequests(app, owner, repo);
+ const openPRs = await getOpenPullRequests(app, owner, repo, options);
if (!Array.isArray(openPRs)) {
return;
}
diff --git a/src/routes.js b/src/routes.js
index 8dfc1a3..e1f3eb2 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -182,12 +182,12 @@ export const routes = {
},
async listPullRequests(req, res, app) {
- const { org, repo } = parseUrlQueryParams(req.url) || {};
+ const { org, repo, page } = parseUrlQueryParams(req.url) || {};
if (!org) {
res.writeHead(400);
return res.end("Please add org parameter in the url e.g. ?org=my-github-org-name");
}
- const prs = await getOpenExternalPullRequests(app, org, repo);
+ const prs = await getOpenExternalPullRequests(app, org, repo, { page: page });
if (req.headers['content-type']?.toLowerCase() === 'application/json') {
res.setHeader('Content-Type', 'application/json');
const jsonString = prs ? JSON.stringify(prs, null, 2) : ("No Open Pull Requests found (or you don't have access to search PRs for " + org);
@@ -209,7 +209,7 @@ export const routes = {
Recent Contributions (Open)
- ${Array.isArray(prs) && prs.length > 0 ? `` : ""}
+ ${Array.isArray(prs) && prs.length > 0 ? `` : ""}
${groupPullRequestsByUser(prs)}
@@ -217,22 +217,32 @@ export const routes = {
${groupPullRequestsByRepo(prs)}
-
-
-