troutbot/src/index.ts
NotAShelf 7d8bc6943d
config: bind to localhost by default
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I00aca92a09291ce12f09da68917f56c06a6a6964
2026-02-01 17:17:33 +03:00

176 lines
5.2 KiB
TypeScript

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 { startPolling } from './polling.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();
if (!process.env.GITHUB_TOKEN) {
logger.error('GITHUB_TOKEN is required for analyze mode');
process.exit(1);
}
initGitHub(process.env.GITHUB_TOKEN);
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 (not needed for polling-only mode)'
);
}
const app = createApp(config);
const port = config.server.port;
const enabledBackends = Object.entries(config.engine.backends)
.filter(([, v]) => v.enabled)
.map(([k]) => k);
const host = config.server.host || '127.0.0.1';
const server = app.listen(port, host, async () => {
logger.info(`Troutbot listening on ${host}:${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'}`);
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
logger.info(`Dashboard available at http://${displayHost}:${port}/dashboard`);
// Start polling if enabled
await startPolling(config);
});
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();
}