Add GitHub integration for PRs, issues and AI PR description (#205)
* feat: integrate GitHub OAuth device flow across runtimes Add GitHub OAuth device flow endpoints across runtimes Introduce GitHubSettings UI panel and sidebar entry Persist GitHub auth state in per-runtime storage * feat: add GitHub PR status and PR description generation Show PR status for the current branch in the Git view Generate a pull request description from the diff between base and head Expose prStatus, prCreate, and prMerge APIs in web and desktop clients * feat: add GitHub PR ready for review Add API to mark pull requests as ready for review Show a Ready button for draft PRs and reflect status in UI Handle token expiration and GraphQL errors when marking ready
This commit is contained in:
committed by
GitHub
parent
0e715be7d6
commit
463e9ec4e3
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
GitHubAPI,
|
||||
GitHubAuthStatus,
|
||||
GitHubPullRequest,
|
||||
GitHubPullRequestCreateInput,
|
||||
GitHubPullRequestMergeInput,
|
||||
GitHubPullRequestMergeResult,
|
||||
GitHubPullRequestReadyInput,
|
||||
GitHubPullRequestReadyResult,
|
||||
GitHubPullRequestStatus,
|
||||
GitHubDeviceFlowComplete,
|
||||
GitHubDeviceFlowStart,
|
||||
GitHubUserSummary,
|
||||
} from '@openchamber/ui/lib/api/types';
|
||||
|
||||
const jsonOrNull = async <T>(response: Response): Promise<T | null> => {
|
||||
return (await response.json().catch(() => null)) as T | null;
|
||||
};
|
||||
|
||||
export const createWebGitHubAPI = (): GitHubAPI => ({
|
||||
async authStatus(): Promise<GitHubAuthStatus> {
|
||||
const response = await fetch('/api/github/auth/status', { method: 'GET', headers: { Accept: 'application/json' } });
|
||||
const payload = await jsonOrNull<GitHubAuthStatus & { error?: string }>(response);
|
||||
if (!response.ok || !payload) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to load GitHub status');
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
|
||||
async authStart(): Promise<GitHubDeviceFlowStart> {
|
||||
const response = await fetch('/api/github/auth/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const payload = await jsonOrNull<GitHubDeviceFlowStart & { error?: string }>(response);
|
||||
if (!response.ok || !payload || !('deviceCode' in payload)) {
|
||||
throw new Error((payload as { error?: string } | null)?.error || response.statusText || 'Failed to start GitHub auth');
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
|
||||
async authComplete(deviceCode: string): Promise<GitHubDeviceFlowComplete> {
|
||||
const response = await fetch('/api/github/auth/complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ deviceCode }),
|
||||
});
|
||||
const payload = await jsonOrNull<GitHubDeviceFlowComplete & { error?: string }>(response);
|
||||
if (!response.ok || !payload) {
|
||||
throw new Error((payload as { error?: string } | null)?.error || response.statusText || 'Failed to complete GitHub auth');
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
|
||||
async authDisconnect(): Promise<{ removed: boolean }> {
|
||||
const response = await fetch('/api/github/auth', { method: 'DELETE', headers: { Accept: 'application/json' } });
|
||||
const payload = await jsonOrNull<{ removed?: boolean; error?: string }>(response);
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to disconnect GitHub');
|
||||
}
|
||||
return { removed: Boolean(payload?.removed) };
|
||||
},
|
||||
|
||||
async me(): Promise<GitHubUserSummary> {
|
||||
const response = await fetch('/api/github/me', { method: 'GET', headers: { Accept: 'application/json' } });
|
||||
const payload = await jsonOrNull<GitHubUserSummary & { error?: string }>(response);
|
||||
if (!response.ok || !payload) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to fetch GitHub user');
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
|
||||
async prStatus(directory: string, branch: string): Promise<GitHubPullRequestStatus> {
|
||||
const response = await fetch(
|
||||
`/api/github/pr/status?directory=${encodeURIComponent(directory)}&branch=${encodeURIComponent(branch)}`,
|
||||
{ method: 'GET', headers: { Accept: 'application/json' } }
|
||||
);
|
||||
const payload = await jsonOrNull<GitHubPullRequestStatus & { error?: string }>(response);
|
||||
if (!response.ok || !payload) {
|
||||
throw new Error(payload?.error || response.statusText || 'Failed to load PR status');
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
|
||||
async prCreate(payload: GitHubPullRequestCreateInput): Promise<GitHubPullRequest> {
|
||||
const response = await fetch('/api/github/pr/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await jsonOrNull<GitHubPullRequest & { error?: string }>(response);
|
||||
if (!response.ok || !body) {
|
||||
throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to create PR');
|
||||
}
|
||||
return body;
|
||||
},
|
||||
|
||||
async prMerge(payload: GitHubPullRequestMergeInput): Promise<GitHubPullRequestMergeResult> {
|
||||
const response = await fetch('/api/github/pr/merge', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await jsonOrNull<GitHubPullRequestMergeResult & { error?: string }>(response);
|
||||
if (!response.ok || !body) {
|
||||
throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to merge PR');
|
||||
}
|
||||
return body;
|
||||
},
|
||||
|
||||
async prReady(payload: GitHubPullRequestReadyInput): Promise<GitHubPullRequestReadyResult> {
|
||||
const response = await fetch('/api/github/pr/ready', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await jsonOrNull<GitHubPullRequestReadyResult & { error?: string }>(response);
|
||||
if (!response.ok || !body) {
|
||||
throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to mark PR ready');
|
||||
}
|
||||
return body;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user