Repo-scoped assignable-user search for GitHub, GitLab, and Gitea, surfaced as
an assignee combobox in the metadata editor and @-mention autocomplete in
forge comment/reply/review surfaces.
- server: GET /api/{provider}/users/search (assignees / project members),
query + directory/override repo resolution, 429 -> 503, connected:false
degradation; GitLab assignee writes resolve login -> ID server-side
- wire: searchUsers (+ searchLabels/milestones/branches/tags) on the three
API clients with tests
- facade: userSearch capability (all three), searchUsers adapters,
mapGithubAssignee/mapGitlabMember/mapGiteaAssignee -> ForgeUser
- ui: ForgeLookupCombobox (keyboard nav, debounced 30s-TTL cache,
connected-only caching), ForgeMentionTextarea (@ token parsing, caret
restore), free-text fallback when lookup is unavailable; i18n in 12 locales
- extras sharing the same infrastructure: GitLab create-issue dialog and
label/milestone/branch/tag lookups in the metadata editor
492 lines
22 KiB
TypeScript
492 lines
22 KiB
TypeScript
import type {
|
|
GitHubAPI,
|
|
GitHubAuthStatus,
|
|
GitHubBranchesSearchResult,
|
|
GitHubIssueCommentsResult,
|
|
GitHubIssueCommentInput,
|
|
GitHubIssueCommentResult,
|
|
GitHubIssueCreateInput,
|
|
GitHubIssueCreateResult,
|
|
GitHubIssueGetResult,
|
|
GitHubIssueUpdateInput,
|
|
GitHubIssueUpdateResult,
|
|
GitHubIssuesListResult,
|
|
GitHubLabelsSearchResult,
|
|
GitHubMilestonesSearchResult,
|
|
GitHubPullRequestContextResult,
|
|
GitHubPullRequestCommitsResult,
|
|
GitHubPullRequestTimelineResult,
|
|
GitHubPullRequestsListResult,
|
|
GitHubPullRequest,
|
|
GitHubPullRequestCreateInput,
|
|
GitHubPullRequestMergeInput,
|
|
GitHubPullRequestMergeResult,
|
|
GitHubPullRequestReadyInput,
|
|
GitHubPullRequestReadyResult,
|
|
GitHubPullRequestUpdateInput,
|
|
GitHubPullRequestStatus,
|
|
GitHubPullRequestReviewInput,
|
|
GitHubPullRequestReviewResult,
|
|
GitHubRepoUpstreamResult,
|
|
GitHubReviewCommentInput,
|
|
GitHubReviewCommentResult,
|
|
GitHubDeviceFlowComplete,
|
|
GitHubDeviceFlowStart,
|
|
GitHubTagsSearchResult,
|
|
GitHubUserSummary,
|
|
GitHubUsersSearchResult,
|
|
} from '@openchamber/ui/lib/api/types';
|
|
import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch';
|
|
import type { RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url';
|
|
|
|
interface WebGitHubAPIOptions {
|
|
urls: RuntimeUrlResolver;
|
|
}
|
|
|
|
const jsonOrNull = async <T>(response: Response): Promise<T | null> => {
|
|
return (await response.json().catch(() => null)) as T | null;
|
|
};
|
|
|
|
export const createWebGitHubAPI = ({ urls }: WebGitHubAPIOptions): GitHubAPI => ({
|
|
async authStatus(): Promise<GitHubAuthStatus> {
|
|
const response = await runtimeFetch('/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 runtimeFetch('/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 runtimeFetch('/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 runtimeFetch('/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 authActivate(accountId: string): Promise<GitHubAuthStatus> {
|
|
const response = await runtimeFetch('/api/github/auth/activate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify({ accountId }),
|
|
});
|
|
const payload = await jsonOrNull<GitHubAuthStatus & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to activate GitHub account');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async authSetGhCliDisabled(disabled: boolean): Promise<{ disabled: boolean }> {
|
|
const response = await runtimeFetch('/api/github/auth/gh-cli', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify({ disabled }),
|
|
});
|
|
const payload = await jsonOrNull<{ disabled?: boolean; error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to update gh CLI setting');
|
|
}
|
|
return { disabled: Boolean(payload.disabled) };
|
|
},
|
|
|
|
async me(): Promise<GitHubUserSummary> {
|
|
const response = await runtimeFetch('/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 searchUsers(directory, query, options): Promise<GitHubUsersSearchResult> {
|
|
const params = new URLSearchParams({ directory, query });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/users/search', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubUsersSearchResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to search GitHub users');
|
|
}
|
|
return { connected: body.connected, repo: body.repo ?? null, users: body.users ?? [] };
|
|
},
|
|
|
|
async searchLabels(directory, query, options): Promise<GitHubLabelsSearchResult> {
|
|
const params = new URLSearchParams({ directory, query });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/labels/search', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubLabelsSearchResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to search GitHub labels');
|
|
}
|
|
return { connected: body.connected, repo: body.repo ?? null, labels: body.labels ?? [] };
|
|
},
|
|
|
|
async searchMilestones(directory, query, options): Promise<GitHubMilestonesSearchResult> {
|
|
const params = new URLSearchParams({ directory, query });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/milestones/search', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubMilestonesSearchResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to search GitHub milestones');
|
|
}
|
|
return { connected: body.connected, repo: body.repo ?? null, milestones: body.milestones ?? [] };
|
|
},
|
|
|
|
async searchBranches(directory, query, options): Promise<GitHubBranchesSearchResult> {
|
|
const params = new URLSearchParams({ directory, query });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/branches/search', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubBranchesSearchResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to search GitHub branches');
|
|
}
|
|
return { connected: body.connected, repo: body.repo ?? null, branches: body.branches ?? [] };
|
|
},
|
|
|
|
async searchTags(directory, query, options): Promise<GitHubTagsSearchResult> {
|
|
const params = new URLSearchParams({ directory, query });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/tags/search', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubTagsSearchResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to search GitHub tags');
|
|
}
|
|
return { connected: body.connected, repo: body.repo ?? null, tags: body.tags ?? [] };
|
|
},
|
|
|
|
async prStatus(directory: string, branch: string, remote?: string, options?: { force?: boolean }): Promise<GitHubPullRequestStatus> {
|
|
const params = new URLSearchParams({
|
|
directory,
|
|
branch,
|
|
...(remote ? { remote } : {}),
|
|
...(options?.force ? { force: 'true' } : {}),
|
|
});
|
|
const response = await runtimeFetch(
|
|
`/api/github/pr/status?${params.toString()}`,
|
|
{ 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 runtimeFetch('/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 prUpdate(payload: GitHubPullRequestUpdateInput): Promise<GitHubPullRequest> {
|
|
const response = await runtimeFetch('/api/github/pr/update', {
|
|
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 update PR');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async prMerge(payload: GitHubPullRequestMergeInput): Promise<GitHubPullRequestMergeResult> {
|
|
const response = await runtimeFetch('/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 runtimeFetch('/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;
|
|
},
|
|
|
|
async repoUpstream(directory: string): Promise<GitHubRepoUpstreamResult> {
|
|
const response = await runtimeFetch(
|
|
`/api/github/repo/upstream?directory=${encodeURIComponent(directory)}`,
|
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
|
);
|
|
const body = await jsonOrNull<GitHubRepoUpstreamResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to detect upstream repo');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async repoBranches(owner: string, repo: string): Promise<string[]> {
|
|
const response = await runtimeFetch(
|
|
`/api/github/repo/branches?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`,
|
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
|
);
|
|
const body = await jsonOrNull<{ branches?: string[]; error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to fetch repo branches');
|
|
}
|
|
return body.branches ?? [];
|
|
},
|
|
|
|
async prsList(directory: string, options?: { page?: number; query?: string }): Promise<GitHubPullRequestsListResult> {
|
|
const page = options?.page ?? 1;
|
|
const params = new URLSearchParams({
|
|
directory,
|
|
page: String(page),
|
|
});
|
|
if (options?.query) {
|
|
params.set('query', options.query);
|
|
}
|
|
const response = await runtimeFetch(
|
|
`/api/github/pulls/list?${params.toString()}`,
|
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
|
);
|
|
const body = await jsonOrNull<GitHubPullRequestsListResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to load pull requests');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async prContext(
|
|
directory: string,
|
|
number: number,
|
|
options?: { includeDiff?: boolean; includeCheckDetails?: boolean; sourceRepo?: { owner: string; repo: string } | null }
|
|
): Promise<GitHubPullRequestContextResult> {
|
|
const params = new URLSearchParams({ directory, number: String(number) });
|
|
if (options?.includeDiff) {
|
|
params.set('diff', '1');
|
|
}
|
|
if (options?.includeCheckDetails) {
|
|
params.set('checkDetails', '1');
|
|
}
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/pulls/context', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const body = await jsonOrNull<GitHubPullRequestContextResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to load pull request context');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async issuesList(directory: string, options?: { page?: number; query?: string }): Promise<GitHubIssuesListResult> {
|
|
const page = options?.page ?? 1;
|
|
const params = new URLSearchParams({
|
|
directory,
|
|
page: String(page),
|
|
});
|
|
if (options?.query) {
|
|
params.set('query', options.query);
|
|
}
|
|
const response = await runtimeFetch(
|
|
`/api/github/issues/list?${params.toString()}`,
|
|
{ method: 'GET', headers: { Accept: 'application/json' } }
|
|
);
|
|
const payload = await jsonOrNull<GitHubIssuesListResult & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to load issues');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async issueGet(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise<GitHubIssueGetResult> {
|
|
const params = new URLSearchParams({ directory, number: String(number) });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/issues/get', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const payload = await jsonOrNull<GitHubIssueGetResult & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to load issue');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async issueComments(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise<GitHubIssueCommentsResult> {
|
|
const params = new URLSearchParams({ directory, number: String(number) });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/issues/comments', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const payload = await jsonOrNull<GitHubIssueCommentsResult & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to load issue comments');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async prCommits(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise<GitHubPullRequestCommitsResult> {
|
|
const params = new URLSearchParams({ directory, number: String(number) });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/pulls/commits', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const payload = await jsonOrNull<GitHubPullRequestCommitsResult & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to load pull request commits');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async prTimeline(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise<GitHubPullRequestTimelineResult> {
|
|
const params = new URLSearchParams({ directory, number: String(number) });
|
|
if (options?.sourceRepo?.owner && options.sourceRepo.repo) {
|
|
params.set('owner', options.sourceRepo.owner);
|
|
params.set('repo', options.sourceRepo.repo);
|
|
}
|
|
const response = await runtimeFetch(urls.api('/api/github/pulls/timeline', params), { method: 'GET', headers: { Accept: 'application/json' } });
|
|
const payload = await jsonOrNull<GitHubPullRequestTimelineResult & { error?: string }>(response);
|
|
if (!response.ok || !payload) {
|
|
throw new Error(payload?.error || response.statusText || 'Failed to load pull request timeline');
|
|
}
|
|
return payload;
|
|
},
|
|
|
|
async issueComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
|
|
const response = await runtimeFetch('/api/github/issues/comment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to post GitHub comment');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async issueCreate(input: GitHubIssueCreateInput): Promise<GitHubIssueCreateResult> {
|
|
const response = await runtimeFetch('/api/github/issues/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubIssueCreateResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to create GitHub issue');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async issueUpdate(input: GitHubIssueUpdateInput): Promise<GitHubIssueUpdateResult> {
|
|
const response = await runtimeFetch('/api/github/issues/update', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubIssueUpdateResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to update GitHub issue');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async prComment(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult> {
|
|
const response = await runtimeFetch('/api/github/pulls/comment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubIssueCommentResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to post GitHub PR comment');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async prReviewComment(input: GitHubReviewCommentInput): Promise<GitHubReviewCommentResult> {
|
|
const response = await runtimeFetch('/api/github/pulls/review-comment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubReviewCommentResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to post GitHub review comment');
|
|
}
|
|
return body;
|
|
},
|
|
|
|
async prSubmitReview(input: GitHubPullRequestReviewInput): Promise<GitHubPullRequestReviewResult> {
|
|
const response = await runtimeFetch('/api/github/pulls/review', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(input),
|
|
});
|
|
const body = await jsonOrNull<GitHubPullRequestReviewResult & { error?: string }>(response);
|
|
if (!response.ok || !body) {
|
|
throw new Error(body?.error || response.statusText || 'Failed to submit GitHub review');
|
|
}
|
|
return body;
|
|
},
|
|
});
|