Skip to content

Commit 4d3cd13

Browse files
feat: setting to mark user contributions internal across org (#69)
1 parent 77089a6 commit 4d3cd13

File tree

6 files changed

+80
-12
lines changed

6 files changed

+80
-12
lines changed

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ WEBSITE_ADDRESS="https://github.app.home"
22
LOGIN_USER=username
33
LOGIN_PASSWORD=strongpassword
44
DEFAULT_GITHUB_ORG=Git-Commit-Show
5+
ONE_CLA_PER_ORG=true
56
GITHUB_BOT_USERS=dependabot[bot],devops-github-rudderstack
67
GITHUB_ORG_MEMBERS=
78
APP_ID="11"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A Node.js server for GitHub app to assist external contributors and save maintai
99
- [x] On `rudder-transformer` PR merge, post a comment to raise PR in `integrations-config`
1010
- [ ] On `integrations-config` PR merge, psot a comment to join Slack's product-releases channel to get notified when that integration goes live
1111
- [ ] On `integrations-config` PR merge, post a comment to raise PR in `rudder-docs`
12+
- [x] List of open PRs by external contributors
1213

1314
## Requirements
1415

app.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import dotenv from "dotenv";
2+
// Load environment variables from .env file
3+
dotenv.config();
24
import fs from "fs";
35
import http from "http";
46
import url from "url";
@@ -23,9 +25,6 @@ try {
2325
console.log(`Application version: ${APP_VERSION}`);
2426
console.log(`Website address: ${process.env.WEBSITE_ADDRESS}`);
2527

26-
// Load environment variables from .env file
27-
dotenv.config();
28-
2928
// Set configured values
3029
const appId = process.env.APP_ID;
3130
// To add GitHub App Private Key directly as a string config (instead of file), convert it to base64 by running following command
@@ -242,6 +241,9 @@ http
242241
case "GET /contributions/pr":
243242
routes.getPullRequestDetail(req, res, app);
244243
break;
244+
case "GET /contributions/reset":
245+
routes.resetContributionData(req, res, app);
246+
break;
245247
case "POST /api/webhook":
246248
middleware(req, res);
247249
break;

src/helpers.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { resolve } from "path";
33
import { PROJECT_ROOT_PATH } from "./config.js";
44
import url from "node:url";
55

6+
function isOneCLAPerOrgEnough() {
7+
return process.env.ONE_CLA_PER_ORG?.toLowerCase()?.trim() === "true" ? true : false;
8+
}
9+
610
export function parseUrlQueryParams(urlString) {
711
if(!urlString) return urlString;
812
try{
@@ -79,15 +83,15 @@ export function isExternalContributionMaybe(pullRequest) {
7983
switch (pullRequest.author_association.toUpperCase()) {
8084
case "OWNER":
8185
pullRequest.isExternalContribution = false;
82-
storage.cache.set(false, username, "contribution", "external", owner, repo);
86+
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
8387
return false;
8488
case "MEMBER":
8589
pullRequest.isExternalContribution = false;
86-
storage.cache.set(false, username, "contribution", "external", owner, repo);
90+
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
8791
return false;
8892
case "COLLABORATOR":
8993
pullRequest.isExternalContribution = false;
90-
storage.cache.set(false, username, "contribution", "external", owner, repo);
94+
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
9195
return false;
9296
default:
9397
//Will need more checks to verify author relation with the repo
@@ -96,15 +100,15 @@ export function isExternalContributionMaybe(pullRequest) {
96100
}
97101
if (pullRequest?.head?.repo?.full_name !== pullRequest?.base?.repo?.full_name) {
98102
pullRequest.isExternalContribution = true;
99-
storage.cache.set(true, username, "contribution", "external", owner, repo);
103+
storage.cache.set(true, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
100104
return true;
101105
} else if (pullRequest?.head?.repo?.full_name && pullRequest?.base?.repo?.full_name) {
102106
pullRequest.isExternalContribution = false;
103-
storage.cache.set(false, username, "contribution", "external", owner, repo);
107+
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
104108
return false;
105109
}
106110
// Utilize cache if possible
107-
const isConfirmedToBeExternalContributionInPast = storage.cache.get(username, "contribution", "external", owner, repo);
111+
const isConfirmedToBeExternalContributionInPast = storage.cache.get(username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
108112
if (typeof isConfirmedToBeExternalContributionInPast === "boolean") {
109113
pullRequest.isExternalContribution = isConfirmedToBeExternalContributionInPast;
110114
return isConfirmedToBeExternalContributionInPast
@@ -126,7 +130,7 @@ async function isExternalContribution(octokit, pullRequest) {
126130
//TODO: Handle failure in checking permissions for the user
127131
const deterministicPermissionCheck = await isAllowedToWriteToTheRepo(octokit, username, owner, repo);
128132
pullRequest.isExternalContribution = deterministicPermissionCheck;
129-
storage.cache.set(deterministicPermissionCheck, username, "contribution", "external", owner, repo);
133+
storage.cache.set(deterministicPermissionCheck, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
130134
return deterministicPermissionCheck;
131135
}
132136

src/routes.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export const routes = {
221221
<br/><br/>
222222
<div class="pagination">
223223
<button class="pagination-button" onclick="goToNextPage()">Next Page...</button>
224+
<a href="/contributions/reset" target="_blank">Reset</button>
224225
</div>
225226
</body>
226227
<script>
@@ -285,6 +286,11 @@ export const routes = {
285286
</script>
286287
</html>`);
287288
},
289+
resetContributionData(req, res, app) {
290+
storage.cache.clear();
291+
res.writeHead(200, { 'Content-Type': 'text/html' });
292+
res.write('Cache cleared');
293+
},
288294
// ${!Array.isArray(prs) || prs?.length < 1 ? "No contributions found! (Might be an access issue)" : prs?.map(pr => `<li><a href="${pr?.user?.html_url}">${pr?.user?.login}</a> contributed a PR - <a href="${pr?.html_url}" target="_blank">${pr?.title}</a> [${pr?.labels?.map(label => label?.name).join('] [')}] <small>updated ${timeAgo(pr?.updated_at)}</small></li>`).join('')}
289295
default(req, res) {
290296
res.writeHead(404);

src/storage.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,57 @@ import { resolve } from "path";
33
import { PROJECT_ROOT_PATH } from "./config.js";
44

55
const dbPath = process.env.DB_PATH || resolve(PROJECT_ROOT_PATH, "db.json");
6+
const cachePath = process.env.CACHE_PATH || resolve(PROJECT_ROOT_PATH, "cache.json");
67
createFileIfMissing(dbPath);
7-
const CACHE = new Map();
8+
createFileIfMissing(cachePath);
9+
const CACHE = initCache();
10+
let lastSnapshotTime = new Date().getTime();
11+
let cacheSnapshotSize = CACHE.size;
12+
const CACHE_SNAPSHOT_INTERVAL = 1000 * 60 * 5;
13+
14+
function initCache() {
15+
try {
16+
const json = fs.readFileSync(cachePath, 'utf-8'); // Read the file as a string
17+
const obj = JSON.parse(json); // Parse JSON back to an object
18+
return new Map(Object.entries(obj)); // Convert Object to a Map
19+
} catch (err) {
20+
return new Map();
21+
}
22+
}
23+
24+
function clearCache() {
25+
CACHE.clear();
26+
fs.truncate(cachePath, 0, (err) => {
27+
if (err) {
28+
console.error('Error truncating cache file:', err);
29+
} else {
30+
console.log('Cache file content deleted successfully.');
31+
}
32+
});
33+
}
34+
35+
async function lazyCacheSnapshot() {
36+
try {
37+
const currentTime = new Date().getTime();
38+
if ((currentTime - lastSnapshotTime) < CACHE_SNAPSHOT_INTERVAL || CACHE.size === cacheSnapshotSize) {
39+
return;
40+
}
41+
const obj = Object.fromEntries(CACHE); // Convert Map to an Object
42+
const json = JSON.stringify(obj, null, 2); // Convert Object to JSON
43+
fs.writeFile(cachePath, json, 'utf-8', function (err) {
44+
if (!err) {
45+
cacheSnapshotSize = CACHE.size;
46+
console.log("Cache saved to file successfully. Total entries: " + cacheSnapshotSize);
47+
} else {
48+
console.error("Unexpected error in saving cache to file. Could be permission related issue.");
49+
}
50+
}); // Write JSON to a file
51+
lastSnapshotTime = currentTime;
52+
console.log(`Cache saved to ${cachePath}`);
53+
} catch (err) {
54+
console.error("Error in saving cache to file");
55+
}
56+
}
857

958
function createFileIfMissing(path) {
1059
try {
@@ -51,7 +100,12 @@ export const storage = {
51100
},
52101
set: function (value, ...args) {
53102
const key = args.join("/");
54-
return CACHE.set(key, value);
103+
let cache = CACHE.set(key, value);
104+
lazyCacheSnapshot();
105+
return cache
106+
},
107+
clear: function () {
108+
clearCache();
55109
}
56110
}
57111
};

0 commit comments

Comments
 (0)