Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I00aca92a09291ce12f09da68917f56c06a6a6964
176 lines
5.2 KiB
TypeScript
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();
|
|
}
|