reviewbot

Source code for the is-a.dev r...
Log | Files | Refs | README | LICENSE

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:
M.env.example | 1+
Mbun.lock | 21+++++++++++++++------
Mindex.js | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackage.json | 7+++++--
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",
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror