Skip to content

Commit

Permalink
feat: setting to mark user contributions internal across org (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
gitcommitshow authored Nov 14, 2024
1 parent 77089a6 commit 4d3cd13
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 12 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ WEBSITE_ADDRESS="https://github.app.home"
LOGIN_USER=username
LOGIN_PASSWORD=strongpassword
DEFAULT_GITHUB_ORG=Git-Commit-Show
ONE_CLA_PER_ORG=true
GITHUB_BOT_USERS=dependabot[bot],devops-github-rudderstack
GITHUB_ORG_MEMBERS=
APP_ID="11"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A Node.js server for GitHub app to assist external contributors and save maintai
- [x] On `rudder-transformer` PR merge, post a comment to raise PR in `integrations-config`
- [ ] On `integrations-config` PR merge, psot a comment to join Slack's product-releases channel to get notified when that integration goes live
- [ ] On `integrations-config` PR merge, post a comment to raise PR in `rudder-docs`
- [x] List of open PRs by external contributors

## Requirements

Expand Down
8 changes: 5 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();
import fs from "fs";
import http from "http";
import url from "url";
Expand All @@ -23,9 +25,6 @@ try {
console.log(`Application version: ${APP_VERSION}`);
console.log(`Website address: ${process.env.WEBSITE_ADDRESS}`);

// Load environment variables from .env file
dotenv.config();

// Set configured values
const appId = process.env.APP_ID;
// To add GitHub App Private Key directly as a string config (instead of file), convert it to base64 by running following command
Expand Down Expand Up @@ -242,6 +241,9 @@ http
case "GET /contributions/pr":
routes.getPullRequestDetail(req, res, app);
break;
case "GET /contributions/reset":
routes.resetContributionData(req, res, app);
break;
case "POST /api/webhook":
middleware(req, res);
break;
Expand Down
18 changes: 11 additions & 7 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { resolve } from "path";
import { PROJECT_ROOT_PATH } from "./config.js";
import url from "node:url";

function isOneCLAPerOrgEnough() {
return process.env.ONE_CLA_PER_ORG?.toLowerCase()?.trim() === "true" ? true : false;
}

export function parseUrlQueryParams(urlString) {
if(!urlString) return urlString;
try{
Expand Down Expand Up @@ -79,15 +83,15 @@ export function isExternalContributionMaybe(pullRequest) {
switch (pullRequest.author_association.toUpperCase()) {
case "OWNER":
pullRequest.isExternalContribution = false;
storage.cache.set(false, username, "contribution", "external", owner, repo);
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return false;
case "MEMBER":
pullRequest.isExternalContribution = false;
storage.cache.set(false, username, "contribution", "external", owner, repo);
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return false;
case "COLLABORATOR":
pullRequest.isExternalContribution = false;
storage.cache.set(false, username, "contribution", "external", owner, repo);
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return false;
default:
//Will need more checks to verify author relation with the repo
Expand All @@ -96,15 +100,15 @@ export function isExternalContributionMaybe(pullRequest) {
}
if (pullRequest?.head?.repo?.full_name !== pullRequest?.base?.repo?.full_name) {
pullRequest.isExternalContribution = true;
storage.cache.set(true, username, "contribution", "external", owner, repo);
storage.cache.set(true, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return true;
} else if (pullRequest?.head?.repo?.full_name && pullRequest?.base?.repo?.full_name) {
pullRequest.isExternalContribution = false;
storage.cache.set(false, username, "contribution", "external", owner, repo);
storage.cache.set(false, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return false;
}
// Utilize cache if possible
const isConfirmedToBeExternalContributionInPast = storage.cache.get(username, "contribution", "external", owner, repo);
const isConfirmedToBeExternalContributionInPast = storage.cache.get(username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
if (typeof isConfirmedToBeExternalContributionInPast === "boolean") {
pullRequest.isExternalContribution = isConfirmedToBeExternalContributionInPast;
return isConfirmedToBeExternalContributionInPast
Expand All @@ -126,7 +130,7 @@ async function isExternalContribution(octokit, pullRequest) {
//TODO: Handle failure in checking permissions for the user
const deterministicPermissionCheck = await isAllowedToWriteToTheRepo(octokit, username, owner, repo);
pullRequest.isExternalContribution = deterministicPermissionCheck;
storage.cache.set(deterministicPermissionCheck, username, "contribution", "external", owner, repo);
storage.cache.set(deterministicPermissionCheck, username, "contribution", "external", owner, isOneCLAPerOrgEnough() ? undefined : repo);
return deterministicPermissionCheck;
}

Expand Down
6 changes: 6 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export const routes = {
<br/><br/>
<div class="pagination">
<button class="pagination-button" onclick="goToNextPage()">Next Page...</button>
<a href="/contributions/reset" target="_blank">Reset</button>
</div>
</body>
<script>
Expand Down Expand Up @@ -285,6 +286,11 @@ export const routes = {
</script>
</html>`);
},
resetContributionData(req, res, app) {
storage.cache.clear();
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write('Cache cleared');
},
// ${!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('')}
default(req, res) {
res.writeHead(404);
Expand Down
58 changes: 56 additions & 2 deletions src/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,57 @@ import { resolve } from "path";
import { PROJECT_ROOT_PATH } from "./config.js";

const dbPath = process.env.DB_PATH || resolve(PROJECT_ROOT_PATH, "db.json");
const cachePath = process.env.CACHE_PATH || resolve(PROJECT_ROOT_PATH, "cache.json");
createFileIfMissing(dbPath);
const CACHE = new Map();
createFileIfMissing(cachePath);
const CACHE = initCache();
let lastSnapshotTime = new Date().getTime();
let cacheSnapshotSize = CACHE.size;
const CACHE_SNAPSHOT_INTERVAL = 1000 * 60 * 5;

function initCache() {
try {
const json = fs.readFileSync(cachePath, 'utf-8'); // Read the file as a string
const obj = JSON.parse(json); // Parse JSON back to an object
return new Map(Object.entries(obj)); // Convert Object to a Map
} catch (err) {
return new Map();
}
}

function clearCache() {
CACHE.clear();
fs.truncate(cachePath, 0, (err) => {
if (err) {
console.error('Error truncating cache file:', err);
} else {
console.log('Cache file content deleted successfully.');
}
});
}

async function lazyCacheSnapshot() {
try {
const currentTime = new Date().getTime();
if ((currentTime - lastSnapshotTime) < CACHE_SNAPSHOT_INTERVAL || CACHE.size === cacheSnapshotSize) {
return;
}
const obj = Object.fromEntries(CACHE); // Convert Map to an Object
const json = JSON.stringify(obj, null, 2); // Convert Object to JSON
fs.writeFile(cachePath, json, 'utf-8', function (err) {
if (!err) {
cacheSnapshotSize = CACHE.size;
console.log("Cache saved to file successfully. Total entries: " + cacheSnapshotSize);
} else {
console.error("Unexpected error in saving cache to file. Could be permission related issue.");
}
}); // Write JSON to a file
lastSnapshotTime = currentTime;
console.log(`Cache saved to ${cachePath}`);
} catch (err) {
console.error("Error in saving cache to file");
}
}

function createFileIfMissing(path) {
try {
Expand Down Expand Up @@ -51,7 +100,12 @@ export const storage = {
},
set: function (value, ...args) {
const key = args.join("/");
return CACHE.set(key, value);
let cache = CACHE.set(key, value);
lazyCacheSnapshot();
return cache
},
clear: function () {
clearCache();
}
}
};

0 comments on commit 4d3cd13

Please sign in to comment.