initial commit

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic08e7c4b5b4f4072de9e2f9a701e977b6a6a6964
This commit is contained in:
raf 2026-01-30 16:46:39 +03:00
commit f8db097ba9
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
21 changed files with 4924 additions and 0 deletions

167
src/index.ts Normal file
View file

@ -0,0 +1,167 @@
import { loadConfig } from './config.js';
import { initLogger, getLogger } from './logger.js';
import {
initGitHub,
fetchPR,
hasExistingComment,
postComment,
updateComment,
formatComment,
} from './github.js';
import { createApp } from './server.js';
import { createEngine } from './engine/index.js';
import type { WebhookEvent } from './types.js';
async function analyzeOne(target: string) {
const match = target.match(/^([^/]+)\/([^#]+)#(\d+)$/);
if (!match) {
console.error('Usage: troutbot analyze <owner/repo#number>');
process.exit(1);
}
const [, owner, repo, numStr] = match;
const prNumber = parseInt(numStr, 10);
const config = loadConfig();
initLogger(config.logging);
const logger = getLogger();
initGitHub(process.env.GITHUB_TOKEN);
if (!process.env.GITHUB_TOKEN) {
logger.error('GITHUB_TOKEN is required for analyze mode');
process.exit(1);
}
const prData = await fetchPR(owner, repo, prNumber);
if (!prData) {
logger.error(`Could not fetch PR ${owner}/${repo}#${prNumber}`);
process.exit(1);
}
const event: WebhookEvent = {
action: 'analyze',
type: 'pull_request',
number: prNumber,
title: prData.title,
body: prData.body,
owner,
repo,
author: prData.author,
labels: prData.labels,
branch: prData.branch,
sha: prData.sha,
};
const engine = createEngine(config.engine);
const analysis = await engine.analyze(event);
logger.info(
`Analyzed ${owner}/${repo}#${prNumber}: impact=${analysis.impact}, confidence=${analysis.confidence.toFixed(2)}`
);
logger.info(`Reasoning: ${analysis.reasoning}`);
const { commentMarker, allowUpdates } = config.response;
const existing = await hasExistingComment(owner, repo, prNumber, commentMarker);
if (existing.exists && !allowUpdates) {
logger.info(`Already commented on ${owner}/${repo}#${prNumber}, skipping`);
return;
}
const body = formatComment(
config.response,
event.type,
analysis.impact,
analysis.confidence,
analysis.reasoning
);
if (existing.exists && allowUpdates && existing.commentId) {
await updateComment(owner, repo, existing.commentId, body);
} else {
await postComment(owner, repo, prNumber, body);
}
}
function serve() {
const config = loadConfig();
initLogger(config.logging);
const logger = getLogger();
initGitHub(process.env.GITHUB_TOKEN);
if (!process.env.GITHUB_TOKEN) {
logger.warn(
'No GITHUB_TOKEN - running in dry-run mode (checks and diff backends will be inactive)'
);
}
if (!process.env.WEBHOOK_SECRET) {
logger.warn('No WEBHOOK_SECRET - webhook signature verification is disabled');
}
const app = createApp(config);
const port = config.server.port;
const enabledBackends = Object.entries(config.engine.backends)
.filter(([, v]) => v.enabled)
.map(([k]) => k);
const server = app.listen(port, () => {
logger.info(`Troutbot listening on port ${port}`);
logger.info(`Enabled backends: ${enabledBackends.join(', ')}`);
// Watched repos
if (config.repositories.length > 0) {
const repos = config.repositories.map((r) => `${r.owner}/${r.repo}`).join(', ');
logger.info(`Watched repos: ${repos}`);
} else {
logger.info('Watched repos: all (no repository filter)');
}
// Active filters (only log non-empty ones)
const { filters } = config;
if (filters.labels.include.length > 0)
logger.info(`Label include filter: ${filters.labels.include.join(', ')}`);
if (filters.labels.exclude.length > 0)
logger.info(`Label exclude filter: ${filters.labels.exclude.join(', ')}`);
if (filters.authors.exclude.length > 0)
logger.info(`Excluded authors: ${filters.authors.exclude.join(', ')}`);
if (filters.branches.include.length > 0)
logger.info(`Branch filter: ${filters.branches.include.join(', ')}`);
// Engine weights and confidence threshold
const { weights, confidenceThreshold } = config.engine;
logger.info(
`Engine weights: checks=${weights.checks}, diff=${weights.diff}, quality=${weights.quality} | threshold=${confidenceThreshold}`
);
// Comment update mode
logger.info(`Comment updates: ${config.response.allowUpdates ? 'enabled' : 'disabled'}`);
logger.info(`Dashboard available at http://localhost:${port}/dashboard`);
});
function shutdown(signal: string) {
logger.info(`Received ${signal}, shutting down gracefully...`);
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
setTimeout(() => {
logger.warn('Graceful shutdown timed out, forcing exit');
process.exit(1);
}, 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
const args = process.argv.slice(2);
if (args[0] === 'analyze' && args[1]) {
analyzeOne(args[1]).catch((err) => {
console.error(err);
process.exit(1);
});
} else {
serve();
}