-
Notifications
You must be signed in to change notification settings - Fork 489
GitHub category #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GitHub category #216
Changes from all commits
9702f14
9586589
3a43033
0c508ce
edef4c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| name: Security Audit | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - '*' | ||
| push: | ||
| branches: | ||
| - main | ||
| - master | ||
| schedule: | ||
| # Run weekly on Mondays at 9 AM UTC | ||
| - cron: '0 9 * * 1' | ||
|
|
||
| jobs: | ||
| audit: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup project | ||
| uses: ./.github/actions/setup-project | ||
| with: | ||
| check-lockfile: 'true' | ||
|
|
||
| - name: Run npm audit | ||
| run: npm audit --audit-level=moderate | ||
| continue-on-error: false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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:([^/]+)\/([^/.]+)/); | ||||||||||
|
Comment on lines
+35
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regular expressions used to capture the repository name are incorrect for repositories that contain a dot in their name. For example, for a URL like
Suggested change
|
||||||||||
|
|
||||||||||
| 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) }); | ||||||||||
| } | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| author: GitHubAuthor; | ||
| createdAt: string; | ||
| labels: GitHubLabel[]; | ||
| url: string; | ||
| body: string; | ||
| } | ||
|
Comment on lines
+9
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Extract duplicated type definitions to a shared module. The interfaces
Centralizing these types in a shared module will prevent definition drift and reduce maintenance burden. 💡 Suggested approachConsider creating a shared types file such as 🤖 Prompt for AI Agents |
||
|
|
||
| export interface ListIssuesResult { | ||
| success: boolean; | ||
| 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 and closed issues in parallel | ||
| const [openResult, closedResult] = await Promise.all([ | ||
| execAsync( | ||
| 'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100', | ||
| { | ||
| cwd: projectPath, | ||
| env: execEnv, | ||
| } | ||
| ), | ||
| execAsync( | ||
| 'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50', | ||
| { | ||
| cwd: projectPath, | ||
| env: execEnv, | ||
| } | ||
| ), | ||
|
Comment on lines
+57
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add timeout to prevent indefinite blocking on CLI commands. The GitHub CLI commands lack timeout protection. If the 🔧 Recommended fixAdd a timeout to the const [openResult, closedResult] = await Promise.all([
execAsync(
'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body --limit 100',
{
cwd: projectPath,
env: execEnv,
+ timeout: 30000, // 30 second timeout
}
),
execAsync(
'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body --limit 50',
{
cwd: projectPath,
env: execEnv,
+ timeout: 30000, // 30 second timeout
}
),
]);🤖 Prompt for AI Agents |
||
| ]); | ||
|
|
||
| const { stdout: openStdout } = openResult; | ||
| const { stdout: closedStdout } = closedResult; | ||
|
|
||
| const openIssues: GitHubIssue[] = JSON.parse(openStdout || '[]'); | ||
| const closedIssues: GitHubIssue[] = JSON.parse(closedStdout || '[]'); | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| openIssues, | ||
| closedIssues, | ||
| }); | ||
| } catch (error) { | ||
| logError(error, 'List GitHub issues failed'); | ||
| res.status(500).json({ success: false, error: getErrorMessage(error) }); | ||
| } | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /** | ||
| * 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| author: GitHubAuthor; | ||
| createdAt: string; | ||
| labels: GitHubLabel[]; | ||
| url: string; | ||
| isDraft: boolean; | ||
| headRefName: string; | ||
| reviewDecision: string | null; | ||
| mergeable: string; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| body: string; | ||
| } | ||
|
|
||
| export interface ListPRsResult { | ||
| success: boolean; | ||
| 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; | ||
| } | ||
|
|
||
| const [openResult, mergedResult] = await Promise.all([ | ||
| 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, | ||
| } | ||
| ), | ||
| 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 { stdout: openStdout } = openResult; | ||
| const { stdout: mergedStdout } = mergedResult; | ||
|
|
||
| const openPRs: GitHubPR[] = JSON.parse(openStdout || '[]'); | ||
| const mergedPRs: GitHubPR[] = JSON.parse(mergedStdout || '[]'); | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| openPRs, | ||
| mergedPRs, | ||
| }); | ||
| } catch (error) { | ||
| logError(error, 'List GitHub PRs failed'); | ||
| res.status(500).json({ success: false, error: getErrorMessage(error) }); | ||
| } | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment indicates this is a
GETendpoint, but it's registered as aPOSTroute inapps/server/src/routes/github/index.ts. The documentation should be consistent with the implementation.