Fix GitHub organization repository support
## Summary - Add support for organization repositories by including org members in repo list API - Add repository reference normalization to handle full GitHub URLs, SSH URLs, and owner/repo format - Apply normalization to all GitHub API calls to ensure consistent behavior ## Changes Made ### 1. Enhanced repo listing (repository-handlers.ts) - Updated `/user/repos` endpoint to include affiliation parameter - Now fetches: owner, collaborator, and organization_member repos - Fixes #20: Organization repos now appear in repository list ### 2. Added URL normalization utility (utils.ts) - New `normalizeRepoReference()` function handles: - owner/repo format (already normalized) - https://github.com/owner/repo URLs - https://github.com/owner/repo.git URLs - git@github.com:owner/repo.git SSH URLs - Prevents 404 errors from malformed repository references ### 3. Applied normalization consistently - Updated repository-handlers.ts to normalize repo refs before API calls - Updated issue-handlers.ts to normalize repo refs before API calls - All GitHub API calls now use normalized repository format ## Testing - Verified with organization repository: imaginationeverywhere/ppsv-charities - Tested with various URL formats - GitHub CLI authentication continues to work seamlessly ## Related Issues Fixes #20: Auto Claude doesn't work with GitHub Organization repositories
This commit is contained in:
@@ -6,7 +6,7 @@ import { ipcMain } from 'electron';
|
|||||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||||
import type { IPCResult, GitHubIssue } from '../../../shared/types';
|
import type { IPCResult, GitHubIssue } from '../../../shared/types';
|
||||||
import { projectStore } from '../../project-store';
|
import { projectStore } from '../../project-store';
|
||||||
import { getGitHubConfig, githubFetch } from './utils';
|
import { getGitHubConfig, githubFetch, normalizeRepoReference } from './utils';
|
||||||
import type { GitHubAPIIssue, GitHubAPIComment } from './types';
|
import type { GitHubAPIIssue, GitHubAPIComment } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,16 +57,24 @@ export function registerGetIssues(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||||
|
if (!normalizedRepo) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const issues = await githubFetch(
|
const issues = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
`/repos/${config.repo}/issues?state=${state}&per_page=100&sort=updated`
|
`/repos/${normalizedRepo}/issues?state=${state}&per_page=100&sort=updated`
|
||||||
) as GitHubAPIIssue[];
|
) as GitHubAPIIssue[];
|
||||||
|
|
||||||
// Filter out pull requests
|
// Filter out pull requests
|
||||||
const issuesOnly = issues.filter(issue => !issue.pull_request);
|
const issuesOnly = issues.filter(issue => !issue.pull_request);
|
||||||
|
|
||||||
const result: GitHubIssue[] = issuesOnly.map(issue =>
|
const result: GitHubIssue[] = issuesOnly.map(issue =>
|
||||||
transformIssue(issue, config.repo)
|
transformIssue(issue, normalizedRepo)
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
@@ -98,12 +106,20 @@ export function registerGetIssue(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||||
|
if (!normalizedRepo) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const issue = await githubFetch(
|
const issue = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
`/repos/${config.repo}/issues/${issueNumber}`
|
`/repos/${normalizedRepo}/issues/${issueNumber}`
|
||||||
) as GitHubAPIIssue;
|
) as GitHubAPIIssue;
|
||||||
|
|
||||||
const result = transformIssue(issue, config.repo);
|
const result = transformIssue(issue, normalizedRepo);
|
||||||
|
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,9 +150,17 @@ export function registerGetIssueComments(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||||
|
if (!normalizedRepo) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const comments = await githubFetch(
|
const comments = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
`/repos/${config.repo}/issues/${issueNumber}/comments`
|
`/repos/${normalizedRepo}/issues/${issueNumber}/comments`
|
||||||
) as GitHubAPIComment[];
|
) as GitHubAPIComment[];
|
||||||
|
|
||||||
return { success: true, data: comments };
|
return { success: true, data: comments };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ipcMain } from 'electron';
|
|||||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||||
import type { IPCResult, GitHubRepository, GitHubSyncStatus } from '../../../shared/types';
|
import type { IPCResult, GitHubRepository, GitHubSyncStatus } from '../../../shared/types';
|
||||||
import { projectStore } from '../../project-store';
|
import { projectStore } from '../../project-store';
|
||||||
import { getGitHubConfig, githubFetch } from './utils';
|
import { getGitHubConfig, githubFetch, normalizeRepoReference } from './utils';
|
||||||
import type { GitHubAPIRepository } from './types';
|
import type { GitHubAPIRepository } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,16 +33,28 @@ export function registerCheckConnection(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Normalize repo reference (handles full URLs, git URLs, etc.)
|
||||||
|
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||||
|
if (!normalizedRepo) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
connected: false,
|
||||||
|
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch repo info
|
// Fetch repo info
|
||||||
const repoData = await githubFetch(
|
const repoData = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
`/repos/${config.repo}`
|
`/repos/${normalizedRepo}`
|
||||||
) as { full_name: string; description?: string };
|
) as { full_name: string; description?: string };
|
||||||
|
|
||||||
// Count open issues
|
// Count open issues
|
||||||
const issuesData = await githubFetch(
|
const issuesData = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
`/repos/${config.repo}/issues?state=open&per_page=1`
|
`/repos/${normalizedRepo}/issues?state=open&per_page=1`
|
||||||
) as unknown[];
|
) as unknown[];
|
||||||
|
|
||||||
const openCount = Array.isArray(issuesData) ? issuesData.length : 0;
|
const openCount = Array.isArray(issuesData) ? issuesData.length : 0;
|
||||||
@@ -71,7 +83,7 @@ export function registerCheckConnection(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of GitHub repositories
|
* Get list of GitHub repositories (personal + organization)
|
||||||
*/
|
*/
|
||||||
export function registerGetRepositories(): void {
|
export function registerGetRepositories(): void {
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
@@ -88,9 +100,11 @@ export function registerGetRepositories(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch user's personal + organization repos
|
||||||
|
// affiliation parameter includes: owner, collaborator, organization_member
|
||||||
const repos = await githubFetch(
|
const repos = await githubFetch(
|
||||||
config.token,
|
config.token,
|
||||||
'/user/repos?per_page=100&sort=updated'
|
'/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member'
|
||||||
) as GitHubAPIRepository[];
|
) as GitHubAPIRepository[];
|
||||||
|
|
||||||
const result: GitHubRepository[] = repos.map(repo => ({
|
const result: GitHubRepository[] = repos.map(repo => ({
|
||||||
|
|||||||
@@ -54,6 +54,32 @@ export function getGitHubConfig(project: Project): GitHubConfig | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a GitHub repository reference to owner/repo format
|
||||||
|
* Handles:
|
||||||
|
* - owner/repo (already normalized)
|
||||||
|
* - https://github.com/owner/repo
|
||||||
|
* - https://github.com/owner/repo.git
|
||||||
|
* - git@github.com:owner/repo.git
|
||||||
|
*/
|
||||||
|
export function normalizeRepoReference(repo: string): string {
|
||||||
|
if (!repo) return '';
|
||||||
|
|
||||||
|
// Remove trailing .git if present
|
||||||
|
let normalized = repo.replace(/\.git$/, '');
|
||||||
|
|
||||||
|
// Handle full GitHub URLs
|
||||||
|
if (normalized.startsWith('https://github.com/')) {
|
||||||
|
normalized = normalized.replace('https://github.com/', '');
|
||||||
|
} else if (normalized.startsWith('http://github.com/')) {
|
||||||
|
normalized = normalized.replace('http://github.com/', '');
|
||||||
|
} else if (normalized.startsWith('git@github.com:')) {
|
||||||
|
normalized = normalized.replace('git@github.com:', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request to the GitHub API
|
* Make a request to the GitHub API
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user