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

187
src/github.ts Normal file
View file

@ -0,0 +1,187 @@
import { Octokit } from '@octokit/rest';
import { getLogger } from './logger.js';
import type { CheckRun, PRFile, ResponseConfig } from './types.js';
let octokit: Octokit | null = null;
export function initGitHub(token?: string): void {
if (!token) {
getLogger().warn('No GITHUB_TOKEN set - running in dry-run mode, comments will not be posted');
return;
}
octokit = new Octokit({ auth: token });
}
export function isDryRun(): boolean {
return octokit === null;
}
// --- Comment operations ---
export async function postComment(
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<void> {
if (!octokit) {
getLogger().info(`[dry-run] Would post comment on ${owner}/${repo}#${issueNumber}:\n${body}`);
return;
}
await octokit.issues.createComment({ owner, repo, issue_number: issueNumber, body });
getLogger().info(`Posted comment on ${owner}/${repo}#${issueNumber}`);
}
export async function hasExistingComment(
owner: string,
repo: string,
issueNumber: number,
marker: string
): Promise<{ exists: boolean; commentId?: number }> {
if (!octokit) {
return { exists: false };
}
const comments = await octokit.paginate(octokit.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
const existing = comments.find((c) => c.body?.includes(marker));
if (existing) {
return { exists: true, commentId: existing.id };
}
return { exists: false };
}
export async function updateComment(
owner: string,
repo: string,
commentId: number,
body: string
): Promise<void> {
if (!octokit) {
getLogger().info(`[dry-run] Would update comment ${commentId}:\n${body}`);
return;
}
await octokit.issues.updateComment({ owner, repo, comment_id: commentId, body });
getLogger().info(`Updated comment ${commentId} on ${owner}/${repo}`);
}
// --- Data fetching for engine backends ---
export async function fetchCheckRuns(
owner: string,
repo: string,
ref: string
): Promise<CheckRun[]> {
if (!octokit) {
getLogger().debug('[dry-run] Cannot fetch check runs without a token');
return [];
}
const response = await octokit.checks.listForRef({
owner,
repo,
ref,
per_page: 100,
});
return response.data.check_runs.map((run) => ({
name: run.name,
status: run.status,
conclusion: run.conclusion,
}));
}
export async function fetchPRFiles(
owner: string,
repo: string,
prNumber: number
): Promise<PRFile[]> {
if (!octokit) {
getLogger().debug('[dry-run] Cannot fetch PR files without a token');
return [];
}
const files = await octokit.paginate(octokit.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
return files.map((f) => ({
filename: f.filename,
additions: f.additions,
deletions: f.deletions,
changes: f.changes,
}));
}
export async function fetchPR(
owner: string,
repo: string,
prNumber: number
): Promise<{
title: string;
body: string;
author: string;
labels: string[];
branch: string;
sha: string;
} | null> {
if (!octokit) return null;
const { data } = await octokit.pulls.get({ owner, repo, pull_number: prNumber });
return {
title: data.title,
body: data.body || '',
author: data.user?.login || '',
labels: (data.labels || []).map((l) => (typeof l === 'string' ? l : l.name || '')),
branch: data.head.ref,
sha: data.head.sha,
};
}
// --- Comment formatting ---
function pickRandom(list: string[]): string {
return list[Math.floor(Math.random() * list.length)];
}
export function formatComment(
responseConfig: ResponseConfig,
type: 'issue' | 'pull_request',
impact: string,
confidence: number,
reasoning: string
): string {
const typeLabel = type === 'pull_request' ? 'pull request' : 'issue';
const { messages } = responseConfig;
let messageList: string[];
if (impact === 'positive') {
messageList = messages.positive;
} else if (impact === 'negative') {
messageList = messages.negative;
} else {
messageList = messages.neutral;
}
const template = pickRandom(messageList);
let body = responseConfig.commentMarker + '\n\n';
body += template.replace(/\{type\}/g, typeLabel).replace(/\{impact\}/g, impact);
if (responseConfig.includeConfidence) {
body += `\n\n**Confidence:** ${(confidence * 100).toFixed(0)}%`;
}
if (responseConfig.includeReasoning) {
body += `\n\n**Analysis:** ${reasoning}`;
}
return body;
}