From 4d3cd13f4897e4a66a33a484c185aa9b803cb42e Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:29:59 +0530 Subject: [PATCH] feat: setting to mark user contributions internal across org (#69) --- .env.sample | 1 + README.md | 1 + app.js | 8 ++++--- src/helpers.js | 18 ++++++++++------ src/routes.js | 6 ++++++ src/storage.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 80 insertions(+), 12 deletions(-) diff --git a/.env.sample b/.env.sample index 402a84d..2f94fc7 100644 --- a/.env.sample +++ b/.env.sample @@ -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" diff --git a/README.md b/README.md index b692677..95c6888 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app.js b/app.js index a5b3cdf..adf71b3 100644 --- a/app.js +++ b/app.js @@ -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"; @@ -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 @@ -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; diff --git a/src/helpers.js b/src/helpers.js index 8ea654f..f64fbfa 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -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{ @@ -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 @@ -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 @@ -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; } diff --git a/src/routes.js b/src/routes.js index 97833ba..8bc6baf 100644 --- a/src/routes.js +++ b/src/routes.js @@ -221,6 +221,7 @@ export const routes = {

`); }, + 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 => `
  • ${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); diff --git a/src/storage.js b/src/storage.js index a6a1b45..e00503a 100644 --- a/src/storage.js +++ b/src/storage.js @@ -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 { @@ -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(); } } };