mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
fixing auto verify for kanban issues
This commit is contained in:
18
apps/server/src/routes/github/index.ts
Normal file
18
apps/server/src/routes/github/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* GitHub routes - HTTP API for GitHub integration
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
|
||||
import { createListIssuesHandler } from './routes/list-issues.js';
|
||||
import { createListPRsHandler } from './routes/list-prs.js';
|
||||
|
||||
export function createGitHubRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post('/check-remote', createCheckGitHubRemoteHandler());
|
||||
router.post('/issues', createListIssuesHandler());
|
||||
router.post('/prs', createListPRsHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
71
apps/server/src/routes/github/routes/check-github-remote.ts
Normal file
71
apps/server/src/routes/github/routes/check-github-remote.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* GET /check-github-remote endpoint - Check if project has a GitHub remote
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||
|
||||
export interface GitHubRemoteStatus {
|
||||
hasGitHubRemote: boolean;
|
||||
remoteUrl: string | null;
|
||||
owner: string | null;
|
||||
repo: string | null;
|
||||
}
|
||||
|
||||
export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemoteStatus> {
|
||||
const status: GitHubRemoteStatus = {
|
||||
hasGitHubRemote: false,
|
||||
remoteUrl: null,
|
||||
owner: null,
|
||||
repo: null,
|
||||
};
|
||||
|
||||
try {
|
||||
// Get the remote URL (origin by default)
|
||||
const { stdout } = await execAsync('git remote get-url origin', {
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
});
|
||||
|
||||
const remoteUrl = stdout.trim();
|
||||
status.remoteUrl = remoteUrl;
|
||||
|
||||
// Check if it's a GitHub URL
|
||||
// Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git
|
||||
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/);
|
||||
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/);
|
||||
|
||||
const match = httpsMatch || sshMatch;
|
||||
if (match) {
|
||||
status.hasGitHubRemote = true;
|
||||
status.owner = match[1];
|
||||
status.repo = match[2].replace(/\.git$/, '');
|
||||
}
|
||||
} catch {
|
||||
// No remote or not a git repo - that's okay
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export function createCheckGitHubRemoteHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body;
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await checkGitHubRemote(projectPath);
|
||||
res.json({
|
||||
success: true,
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Check GitHub remote failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
35
apps/server/src/routes/github/routes/common.ts
Normal file
35
apps/server/src/routes/github/routes/common.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Common utilities for GitHub routes
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
export const execAsync = promisify(exec);
|
||||
|
||||
// Extended PATH to include common tool installation locations
|
||||
export const extendedPath = [
|
||||
process.env.PATH,
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/home/linuxbrew/.linuxbrew/bin',
|
||||
`${process.env.HOME}/.local/bin`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':');
|
||||
|
||||
export const execEnv = {
|
||||
...process.env,
|
||||
PATH: extendedPath,
|
||||
};
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function logError(error: unknown, context: string): void {
|
||||
console.error(`[GitHub] ${context}:`, error);
|
||||
}
|
||||
89
apps/server/src/routes/github/routes/list-issues.ts
Normal file
89
apps/server/src/routes/github/routes/list-issues.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* POST /list-issues endpoint - List GitHub issues for a project
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||
import { checkGitHubRemote } from './check-github-remote.js';
|
||||
|
||||
export interface GitHubLabel {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface GitHubAuthor {
|
||||
login: string;
|
||||
}
|
||||
|
||||
export interface GitHubIssue {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
labels: GitHubLabel[];
|
||||
url: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ListIssuesResult {
|
||||
success: boolean;
|
||||
issues?: GitHubIssue[];
|
||||
openIssues?: GitHubIssue[];
|
||||
closedIssues?: GitHubIssue[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function createListIssuesHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body;
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if this is a GitHub repo
|
||||
const remoteStatus = await checkGitHubRemote(projectPath);
|
||||
if (!remoteStatus.hasGitHubRemote) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Project does not have a GitHub remote',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch open issues
|
||||
const { stdout: openStdout } = await execAsync(
|
||||
'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100',
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch closed issues
|
||||
const { stdout: closedStdout } = await execAsync(
|
||||
'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50',
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
);
|
||||
|
||||
const openIssues: GitHubIssue[] = JSON.parse(openStdout || '[]');
|
||||
const closedIssues: GitHubIssue[] = JSON.parse(closedStdout || '[]');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
openIssues,
|
||||
closedIssues,
|
||||
issues: [...openIssues, ...closedIssues],
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'List GitHub issues failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
93
apps/server/src/routes/github/routes/list-prs.ts
Normal file
93
apps/server/src/routes/github/routes/list-prs.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* POST /list-prs endpoint - List GitHub pull requests for a project
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
|
||||
import { checkGitHubRemote } from './check-github-remote.js';
|
||||
|
||||
export interface GitHubLabel {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface GitHubAuthor {
|
||||
login: string;
|
||||
}
|
||||
|
||||
export interface GitHubPR {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
labels: GitHubLabel[];
|
||||
url: string;
|
||||
isDraft: boolean;
|
||||
headRefName: string;
|
||||
reviewDecision: string | null;
|
||||
mergeable: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ListPRsResult {
|
||||
success: boolean;
|
||||
prs?: GitHubPR[];
|
||||
openPRs?: GitHubPR[];
|
||||
mergedPRs?: GitHubPR[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function createListPRsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath } = req.body;
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// First check if this is a GitHub repo
|
||||
const remoteStatus = await checkGitHubRemote(projectPath);
|
||||
if (!remoteStatus.hasGitHubRemote) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Project does not have a GitHub remote',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch open PRs
|
||||
const { stdout: openStdout } = await execAsync(
|
||||
'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100',
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch merged PRs
|
||||
const { stdout: mergedStdout } = await execAsync(
|
||||
'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50',
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: execEnv,
|
||||
}
|
||||
);
|
||||
|
||||
const openPRs: GitHubPR[] = JSON.parse(openStdout || '[]');
|
||||
const mergedPRs: GitHubPR[] = JSON.parse(mergedStdout || '[]');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
openPRs,
|
||||
mergedPRs,
|
||||
prs: [...openPRs, ...mergedPRs],
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'List GitHub PRs failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -23,10 +23,6 @@ const suggestionsSchema = {
|
||||
id: { type: 'string' },
|
||||
category: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
steps: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
priority: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
@@ -34,7 +30,7 @@ const suggestionsSchema = {
|
||||
},
|
||||
reasoning: { type: 'string' },
|
||||
},
|
||||
required: ['category', 'description', 'steps', 'priority', 'reasoning'],
|
||||
required: ['category', 'description', 'priority', 'reasoning'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -62,9 +58,8 @@ Look at the codebase and provide 3-5 concrete suggestions.
|
||||
For each suggestion, provide:
|
||||
1. A category (e.g., "User Experience", "Security", "Performance")
|
||||
2. A clear description of what to implement
|
||||
3. Concrete steps to implement it
|
||||
4. Priority (1=high, 2=medium, 3=low)
|
||||
5. Brief reasoning for why this would help
|
||||
3. Priority (1=high, 2=medium, 3=low)
|
||||
4. Brief reasoning for why this would help
|
||||
|
||||
The response will be automatically formatted as structured JSON.`;
|
||||
|
||||
@@ -164,7 +159,6 @@ The response will be automatically formatted as structured JSON.`;
|
||||
id: `suggestion-${Date.now()}-0`,
|
||||
category: 'Analysis',
|
||||
description: 'Review the AI analysis output for insights',
|
||||
steps: ['Review the generated analysis'],
|
||||
priority: 1,
|
||||
reasoning: 'The AI provided analysis but suggestions need manual review',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user