initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ic08e7c4b5b4f4072de9e2f9a701e977b6a6a6964
This commit is contained in:
commit
f8db097ba9
21 changed files with 4924 additions and 0 deletions
187
src/github.ts
Normal file
187
src/github.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue