commit 7bedbcd6571612765b4245049cdef847a36402b3
parent 1b0051ed16f74b7efdd9f18b2c9820abbb00bea0
Author: iostpa <iostpa@iostpa.com>
Date: Sat, 30 May 2026 10:23:25 +0300
Merge pull request #55 from iostpa/improvesystem
Add automatic cleanups and checks for PRs
Diffstat:
4 files changed, 145 insertions(+), 21 deletions(-)
diff --git a/.env.example b/.env.example
@@ -4,6 +4,7 @@ PRIVATE_KEY_PATH="private-key.pem"
WEBHOOK_SECRET=""
SENTRY=""
PORT="3000"
+INSTALLATION_ID=""
SQL_HOST=""
SQL_PORT=""
SQL_USER=""
diff --git a/bun.lock b/bun.lock
@@ -6,7 +6,10 @@
"name": "reviewbot",
"dependencies": {
"@fastify/middie": "^9.3.2",
- "@sentry/bun": "^10.54.0",
+ "@octokit/auth-app": "^8.2.0",
+ "@octokit/core": "^7.0.6",
+ "@sentry/bun": "^10.55.0",
+ "cron": "^4.4.0",
"dotenv": "^17.4.2",
"fastify": "^5.8.5",
"mariadb": "^3.5.2",
@@ -125,15 +128,15 @@
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
- "@sentry/bun": ["@sentry/bun@10.54.0", "", { "dependencies": { "@sentry/core": "10.54.0", "@sentry/node": "10.54.0" } }, "sha512-xD3E+Aj+W7D2PCLSEcK2OkNWe659vKb50jKgX9nv2eTmI02O7XGxpyKQqVJVcvLB4xKKLtDEd2AxCmB0o3+NVA=="],
+ "@sentry/bun": ["@sentry/bun@10.55.0", "", { "dependencies": { "@sentry/core": "10.55.0", "@sentry/node": "10.55.0" } }, "sha512-NuuQYdSb8LtUDN8Hj+7GHgs4OkUmKH0YTLcyb1cIcuOwfWtLYizj0mQDanRHS2LoCHriXxj60JMvtfS8bGobTA=="],
- "@sentry/core": ["@sentry/core@10.54.0", "", {}, "sha512-yC/bc8N5ut6vk9X/ugTnIFAbzaSZ2uGoKiHRGzt7VseDIrjXk5ENDJP0m7Rbchuozr41kBv2QB3mPcHUhfB43w=="],
+ "@sentry/core": ["@sentry/core@10.55.0", "", {}, "sha512-XUyoNtDSYCvgJnoNzlh+YeAXfIPhCRIXbhWqqM3GQ3AFtZICi85lkyfsrwXEl9wzlPGYnU+Eg8F4tOfScx+FcQ=="],
- "@sentry/node": ["@sentry/node@10.54.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@sentry/core": "10.54.0", "@sentry/node-core": "10.54.0", "@sentry/opentelemetry": "10.54.0", "import-in-the-middle": "^3.0.0" } }, "sha512-Jc31dMBs9aBUv6TXmIPNwv2u18YbfvWQG32IkM3dFWAAoJQhCqLZfN0MEDSf9TeNexIf8qBMZtJRHgHIrWYiGg=="],
+ "@sentry/node": ["@sentry/node@10.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@sentry/core": "10.55.0", "@sentry/node-core": "10.55.0", "@sentry/opentelemetry": "10.55.0", "import-in-the-middle": "^3.0.0" } }, "sha512-+fB/ByoHVWPLGgoafYciiMatTNyX1FHj1bsqZBN+Pw3McbuEU1nwCPLt9zuyZZiWlQtXKsyuACS4ZhXnID5l8A=="],
- "@sentry/node-core": ["@sentry/node-core@10.54.0", "", { "dependencies": { "@sentry/core": "10.54.0", "@sentry/opentelemetry": "10.54.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-QR5RnIK78g0Np2+VWMZ3TatM7C+oX9zIQ1W36o3KOjw0nNcXkWjZT1lEu4me8cp2s8s3hA4qT7fwcciQqkj1UQ=="],
+ "@sentry/node-core": ["@sentry/node-core@10.55.0", "", { "dependencies": { "@sentry/core": "10.55.0", "@sentry/opentelemetry": "10.55.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-M8XMMIk9Y0PGZoEt37Oe5dQCdqDdJlBcwLXidpz/s5k4QtJvCO/BbtcivcuKI2htw5FwxJkSrHUzRvT36tlDpg=="],
- "@sentry/opentelemetry": ["@sentry/opentelemetry@10.54.0", "", { "dependencies": { "@sentry/core": "10.54.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-58Jk9yMos5DwhamDsNmnoQMSNx0yD9E+h1pZwkw34ve2qB9tv+cys3Oz6nfazT9ZdIsXIgpQntN8AfMXAvv4/g=="],
+ "@sentry/opentelemetry": ["@sentry/opentelemetry@10.55.0", "", { "dependencies": { "@sentry/core": "10.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-0+YrNmVNrttki4rWP4DW+UTt5MziepwDLNBde39tgc3cGCcy5fLSdDfhb4JfTaE5TXt4kd5XrkgvS/sDgm3RZg=="],
"@types/aws-lambda": ["@types/aws-lambda@8.10.147", "", {}, ""],
@@ -145,6 +148,8 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+ "@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
+
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, ""],
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
@@ -175,6 +180,8 @@
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+ "cron": ["cron@4.4.0", "", { "dependencies": { "@types/luxon": "~3.7.0", "luxon": "~3.7.0" } }, "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ=="],
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -279,6 +286,8 @@
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+ "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
+
"mariadb": ["mariadb@3.5.2", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": ">=18", "denque": "^2.1.0", "iconv-lite": "^0.7.2", "lru-cache": "^10.4.3" } }, "sha512-9rztrI4nouxAY/82a+RlzzZ5ie2vxu2eYclkBvTy1ATXH1B9cnvZ0O71Pzsy/mlfDb5P3HhOg0JzQKkDRhctyA=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
diff --git a/index.js b/index.js
@@ -4,7 +4,10 @@ import fs from 'fs';
import middie from '@fastify/middie';
import Fastify from 'fastify';
import { App } from 'octokit';
+import { Octokit } from "@octokit/core";
import { createNodeMiddleware } from '@octokit/webhooks';
+import { createAppAuth } from "@octokit/auth-app";
+import { CronJob } from 'cron';
const mariadb = require('mariadb');
// Load environment variables from .env file
@@ -14,8 +17,10 @@ dotenv.config();
const sentryDsn = process.env.SENTRY;
const appId = process.env.APP_ID;
const privateKeyPath = process.env.PRIVATE_KEY_PATH;
+const installationId = process.env.INSTALLATION_ID;
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const secret = process.env.WEBHOOK_SECRET;
+const numberOfDays = 3;
const newPRs = fs.readFileSync('./message/opened.md', 'utf8');
const mergedPRs = fs.readFileSync('./message/merged.md', 'utf8');
const draftPRs = fs.readFileSync('./message/draft.md', 'utf8');
@@ -23,6 +28,7 @@ const lowPriorityMessage = fs.readFileSync('./message/label/lowpriority.md', 'ut
const ignoreLabels = ["maintainer"];
const reasonLabels = ["reason: abuse risk", "reason: commercial usage", "reason: impersonation", "reason: inaccessible website", "reason: incomplete pr", "reason: incomplete website", "reason: invalid file", "reason: invalid records", "reason: invalid social", "reason: merge conflict", "reason: not dev related", "reason: nsfw", "reason: other", "reason: unauthorized", "reason: incompatible records"];
const unremovableLabels = ["maintainer", "ci: bypass-owner-check", "no-stale", "r: william"];
+const reviewerUsernames = ["DEV-DIBSTER", "dragsbruh", "iostpa", "notamitgamer", "omsenjalia", "orangci", "satr14washere", "Stef-00012", "STICKnoLOGIC", "wdhdev", "Yunexiz"];
Sentry.init({
dsn: sentryDsn,
@@ -34,6 +40,7 @@ Sentry.init({
});
// Database
+// Following order of columns: username, prnumber, time, repoowner, repo
const pool = mariadb.createPool({
user: process.env.SQL_USER,
password: process.env.SQL_PASSWORD,
@@ -53,12 +60,84 @@ const app = new App({
}
});
+// for cron
+const installationOctokit = new Octokit({
+ authStrategy: createAppAuth,
+ auth: {
+ appId,
+ privateKey,
+ installationId,
+ },
+});
+
// Get & log the authenticated app's name
const { data } = await app.octokit.request('/app');
// https://github.com/octokit/core.js#logging
app.octokit.log.debug(`Authenticated as '${data.name}'`);
+
+function getNumberOfDays(start, end) {
+ const date1 = new Date(start);
+ const date2 = new Date(end);
+
+ // One day in milliseconds
+ const oneDay = 1000 * 60 * 60 * 24;
+
+ // Calculating the time difference between two dates
+ const diffInTime = date2.getTime() - date1.getTime();
+
+ // Calculating the no. of days between two dates
+ const diffInDays = Math.round(diffInTime / oneDay);
+
+ return diffInDays;
+};
+
+
+// Check if a low priority pull request has been in the database for over 3 days
+let job = new CronJob(
+ '0 * * * *', // cronTime
+ async function () {
+ let date = new Date();
+ let conn;
+ try {
+ conn = await pool.getConnection();
+ let res = await conn.query(`SELECT * FROM LIST`);
+ let resJson = JSON.stringify(res);
+ if (resJson !== '[]') {
+ let parsed = JSON.parse(resJson);
+ for (let i in parsed) {
+ if (getNumberOfDays(parsed[i].time, date) >= numberOfDays) {
+ await conn.query(`DELETE FROM LIST WHERE time=(?)`, [parsed[i].time]);
+ await installationOctokit.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', {
+ owner: parsed[i].repoowner,
+ repo: parsed[i].repo,
+ issue_number: parsed[i].prnumber,
+ name: "status: low priority",
+ });
+ console.log(`Removed #${parsed[i].prnumber} from the database since it has existed for 3 or more days.`);
+ }
+ }
+ } else if (resJson === '[]') {
+ return;
+ }
+ } catch (error) {
+ Sentry.captureException(error);
+ fastify.log.error(error);
+ if (error.response) {
+ console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`);
+ } else {
+ console.error(error);
+ }
+ } finally {
+ if (conn) conn.end();
+ }
+ }, // onTick
+ null, // onComplete
+ true, // start
+ 'Europe/Bucharest' // timeZone
+);
+
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request
app.webhooks.on('pull_request.opened', async ({ octokit, payload }) => {
console.log(`Received a open pull request event for #${payload.pull_request.number} on https://github.com/${payload.repository.full_name}`);
@@ -97,7 +176,8 @@ app.webhooks.on('pull_request.opened', async ({ octokit, payload }) => {
let resJson = JSON.stringify(res);
if (resJson !== "[]") {
let parsed = JSON.parse(resJson);
- if (parsed[0].username === payload.pull_request.user.login) {
+ let date = new Date();
+ if (parsed[0].username === payload.pull_request.user.login && getNumberOfDays(parsed[0].time, date) <= numberOfDays) {
let lowPriority = `
# Low priority
@@ -119,7 +199,18 @@ If you think this is a mistake then please contact [iostpa](https://github.com/i
issue_number: payload.pull_request.number,
body: lowPriority
});
- console.log(`Auto-added and sent low priority message to #${payload.pull_request.number} on https://github.com/${payload.repository.full_name} because it was found in the database.`);
+ await conn.query(`UPDATE LIST SET prnumber=(?) WHERE prnumber=(?)`, [payload.pull_request.number, parsed[0].prnumber]);
+ await conn.query(`UPDATE LIST SET time=(?) WHERE prnumber=(?)`, [payload.pull_request.created_at, parsed[0].prnumber]);
+ console.log(`Auto-added, replaced with new PR number and sent low priority message to #${payload.pull_request.number} on https://github.com/${payload.repository.full_name} because it was found in the database.`);
+ } else if (parsed[0].username === payload.pull_request.user.login && getNumberOfDays(parsed[0].time, date) >= numberOfDays) {
+ await octokit.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', {
+ owner: payload.repository.owner.login,
+ repo: payload.repository.name,
+ issue_number: payload.pull_request.number,
+ name: "status: low priority",
+ });
+ res = await conn.query("DELETE FROM LIST WHERE username=(?)", [parsed[0].username]);
+ console.log(`Removed #${payload.pull_request.number} from https://github.com/${payload.repository.full_name} from the low priority database as well as the label.`);
}
} else if (resJson === '[]') {
return;
@@ -204,7 +295,7 @@ If you need any help, please create an issue or ask our team in the [Discord ser
issue_number: payload.pull_request.number,
body: body
});
- console.log(`Sent reason messages at #${payload.pull_request.number} from ${payload.repository.name}`);
+ console.log(`Sent reason messages at #${payload.pull_request.number} from https://github.com/${payload.repository.full_name}`);
if (denied === true) {
await octokit.request('PATCH /repos/{owner}/{repo}/pulls/{pull_number}', {
owner: payload.repository.owner.login,
@@ -212,7 +303,7 @@ If you need any help, please create an issue or ask our team in the [Discord ser
pull_number: payload.pull_request.number,
state: 'closed',
});
- console.log(`Closed pull request at #${payload.pull_request.number} from ${payload.repository.name}`);
+ console.log(`Closed pull request at #${payload.pull_request.number} from https://github.com/${payload.repository.full_name}`);
};
} catch (error) {
Sentry.captureException(error);
@@ -236,11 +327,9 @@ If you need any help, please create an issue or ask our team in the [Discord ser
issue_number: payload.pull_request.number,
body: lowPriorityMessage
});
- console.log(`Sent low priority message to #${payload.pull_request.number} from ${payload.repository.name}`);
- res = await conn.query("INSERT INTO LIST VALUES (?, ?)", [payload.pull_request.user.login, payload.pull_request.number]);
- console.log(`Logged #${payload.pull_request.number} from ${payload.repository.name} to the low priority database.`);
- } else {
- return;
+ console.log(`Sent low priority message to #${payload.pull_request.number} from https://github.com/${payload.repository.full_name}`);
+ await conn.query("INSERT INTO LIST VALUES (?, ?, ?, ?, ?)", [payload.pull_request.user.login, payload.pull_request.number, payload.pull_request.created_at, payload.repository.owner.login, payload.repository.name]);
+ console.log(`Logged #${payload.pull_request.number} from https://github.com/${payload.repository.full_name} to the low priority database.`);
}
} catch (error) {
Sentry.captureException(error);
@@ -259,6 +348,7 @@ If you need any help, please create an issue or ask our team in the [Discord ser
// https://github.com/octokit/webhooks.js/?tab=readme-ov-file#webhook-events
app.webhooks.on('pull_request.closed', async ({ octokit, payload }) => {
console.log(`Received a closed pull request event for #${payload.pull_request.number} on https://github.com/${payload.repository.full_name}`);
+ let conn;
try {
if (payload.pull_request.merged === true) {
const labels = await octokit.rest.issues.listLabelsOnIssue({
@@ -291,22 +381,41 @@ app.webhooks.on('pull_request.closed', async ({ octokit, payload }) => {
if (labelData[i].name) {
listOfLabels.push(labelData[i].name);
}
- }
+ }
for (let i in listOfLabels) {
- if (!listOfLabels.includes("maintainer")) {
+ if (!listOfLabels.includes(unremovableLabels)) {
await octokit.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', {
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: payload.pull_request.number,
name: listOfLabels[i],
});
+ } else if (listOfLabels[i] == "status: low priority") {
+ return;
} else if (unremovableLabels.includes(listOfLabels[i])) {
return;
}
}
console.log(`Removed all labels from #${payload.pull_request.number} on https://github.com/${payload.repository.full_name}`);
- } else { return; };
+ } else {
+ conn = await pool.getConnection();
+ let res = await conn.query(`SELECT * FROM LIST WHERE username=(?)`, [payload.pull_request.user.login]);
+ let resJson = JSON.stringify(res);
+ if (resJson !== "[]") {
+ let parsed = JSON.parse(resJson);
+ if (reviewerUsernames.includes(payload.sender.login)) {
+ res = await conn.query("DELETE FROM LIST WHERE username=(?)", [parsed[0].username]);
+ await octokit.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', {
+ owner: payload.repository.owner.login,
+ repo: payload.repository.name,
+ issue_number: payload.pull_request.number,
+ name: "status: low priority",
+ });
+ console.log(`Removed #${payload.pull_request.number} from https://github.com/${payload.repository.full_name} from the low priority database as well as the label.`);
+ }
+ }
+ };
} catch (error) {
Sentry.captureException(error);
fastify.log.error(error);
@@ -315,6 +424,8 @@ app.webhooks.on('pull_request.closed', async ({ octokit, payload }) => {
} else {
console.error(error);
}
+ } finally {
+ if (conn) conn.end();
}
});
@@ -327,7 +438,7 @@ app.webhooks.on('pull_request.unlabeled', async ({ payload }) => {
let resJson = JSON.stringify(res);
if (resJson !== "[]") {
res = await conn.query("DELETE FROM LIST WHERE username=(?)", [payload.pull_request.user.login]);
- console.log(`Removed #${payload.pull_request.number} from ${payload.repository.name} from the low priority database.`);
+ console.log(`Removed #${payload.pull_request.number} from https://github.com/${payload.repository.full_name} from the low priority database.`);
} else {
return;
}
diff --git a/package.json b/package.json
@@ -1,7 +1,7 @@
{
"name": "reviewbot",
"private": true,
- "version": "1.3.1",
+ "version": "1.4.0",
"type": "module",
"description": "An app to assist in the review process of is-a-dev",
"author": "iostpa",
@@ -17,7 +17,10 @@
},
"dependencies": {
"@fastify/middie": "^9.3.2",
- "@sentry/bun": "^10.54.0",
+ "@octokit/auth-app": "^8.2.0",
+ "@octokit/core": "^7.0.6",
+ "@sentry/bun": "^10.55.0",
+ "cron": "^4.4.0",
"dotenv": "^17.4.2",
"fastify": "^5.8.5",
"mariadb": "^3.5.2",