Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: setting to mark user contributions internal across org #69

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
};