Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 364
experiment(ask_gh): Support on demand indexing of GitHub repositories#785
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
fd9973a9f9ce5fdb8adf5f8e5a84a8dc2ae82a6616c10e10dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -7,6 +7,9 @@ import z from 'zod'; | ||||||||||||||||||
| import { ConnectionManager } from './connectionManager.js'; | ||||||||||||||||||
| import { PromClient } from './promClient.js'; | ||||||||||||||||||
| import { RepoIndexManager } from './repoIndexManager.js'; | ||||||||||||||||||
| import { createGitHubRepoRecord } from './repoCompileUtils.js'; | ||||||||||||||||||
| import { Octokit } from '@octokit/rest'; | ||||||||||||||||||
| import { SINGLE_TENANT_ORG_ID } from './constants.js'; | ||||||||||||||||||
| const logger = createLogger('api'); | ||||||||||||||||||
| const PORT = 3060; | ||||||||||||||||||
| @@ -33,6 +36,7 @@ export class Api { | ||||||||||||||||||
| app.post('/api/sync-connection', this.syncConnection.bind(this)); | ||||||||||||||||||
| app.post('/api/index-repo', this.indexRepo.bind(this)); | ||||||||||||||||||
| app.post(`/api/experimental/add-github-repo`, this.experimental_addGithubRepo.bind(this)); | ||||||||||||||||||
| this.server = app.listen(PORT, () => { | ||||||||||||||||||
| logger.info(`API server is running on port ${PORT}`); | ||||||||||||||||||
| @@ -92,6 +96,47 @@ export class Api { | ||||||||||||||||||
| res.status(200).json({ jobId }); | ||||||||||||||||||
| } | ||||||||||||||||||
| private async experimental_addGithubRepo(req: Request, res: Response) { | ||||||||||||||||||
| const schema = z.object({ | ||||||||||||||||||
| owner: z.string(), | ||||||||||||||||||
| repo: z.string(), | ||||||||||||||||||
| }).strict(); | ||||||||||||||||||
Comment on lines
+100
to
+103
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. 🧩 Analysis chain🏁 Script executed: # First, let's examine the file around the specified lines
cat -n packages/backend/src/api.ts | sed -n '95,110p'Repository: sourcebot-dev/sourcebot Length of output: 698 🏁 Script executed: # Let's also check the full context of this schema usage
cat -n packages/backend/src/api.ts | sed -n '90,120p'Repository: sourcebot-dev/sourcebot Length of output: 1265 🌐 Web query:
💡 Result: In Zod, both
Typical usage to require a non-empty string after trimming: import{z}from"zod";constschema=z.string().trim().min(1,{message: "Required"});(Chaining is the standard Zod API style—e.g., Sources: Zod strings guide [1], Zod v4 package docs [2]. Tighten owner/repo validation. Empty strings (including whitespace-only strings) currently pass validation and would fail when passed to the GitHub API. Add 🔧 Suggested fix- const schema = z.object({- owner: z.string(),- repo: z.string(),- }).strict();+ const schema = z.object({+ owner: z.string().trim().min(1),+ repo: z.string().trim().min(1),+ }).strict();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||
| const parsed = schema.safeParse(req.body); | ||||||||||||||||||
| if (!parsed.success) { | ||||||||||||||||||
| res.status(400).json({ error: parsed.error.message }); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| const octokit = new Octokit(); | ||||||||||||||||||
| const response = await octokit.rest.repos.get({ | ||||||||||||||||||
| owner: parsed.data.owner, | ||||||||||||||||||
| repo: parsed.data.repo, | ||||||||||||||||||
| }); | ||||||||||||||||||
| const record = createGitHubRepoRecord({ | ||||||||||||||||||
| repo: response.data, | ||||||||||||||||||
| hostUrl: 'https://github.com', | ||||||||||||||||||
| isAutoCleanupDisabled: true, | ||||||||||||||||||
| }); | ||||||||||||||||||
| const repo = await this.prisma.repo.upsert({ | ||||||||||||||||||
| where: { | ||||||||||||||||||
| external_id_external_codeHostUrl_orgId: { | ||||||||||||||||||
| external_id: record.external_id, | ||||||||||||||||||
| external_codeHostUrl: record.external_codeHostUrl, | ||||||||||||||||||
| orgId: SINGLE_TENANT_ORG_ID, | ||||||||||||||||||
| } | ||||||||||||||||||
| }, | ||||||||||||||||||
| update: record, | ||||||||||||||||||
| create: record, | ||||||||||||||||||
| }); | ||||||||||||||||||
| const [jobId ] = await this.repoIndexManager.createJobs([repo], RepoIndexingJobType.INDEX); | ||||||||||||||||||
| res.status(200).json({ jobId, repoId: repo.id }); | ||||||||||||||||||
Comment on lines
+111
to
+137
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. 🧩 Analysis chain🏁 Script executed: # Check if file exists and view the content around lines 111-137if [ -f"packages/backend/src/api.ts" ];thenecho"File found. Checking lines 111-137:"
sed -n '111,137p' packages/backend/src/api.ts
echo""echo"Broader context (lines 100-150):"
wc -l packages/backend/src/api.ts
elseecho"File not found. Searching for api.ts files:"
find . -name "api.ts" -type f 2>/dev/null | head -20
fiRepository: sourcebot-dev/sourcebot Length of output: 1094 🏁 Script executed: # Check for imports and logger availabilityif [ -f"packages/backend/src/api.ts" ];thenecho"=== Imports and class context ==="
head -50 packages/backend/src/api.ts
fiRepository: sourcebot-dev/sourcebot Length of output: 1925 🏁 Script executed: # Search for existing error handling patterns in the codebaseecho"=== Searching for octokit usage patterns ==="
rg -A 5 "new Octokit" --type ts packages/backend/src/Repository: sourcebot-dev/sourcebot Length of output: 805 🌐 Web query:
💡 Result: In Octokit JS, GitHub API failures are typically thrown as a
How to interpret / handle the status codes404 Not Found
403 Forbidden
429 Too Many Requests
Rate limit signals to check (403/429)GitHub’s guidance when rate-limited: (docs.github.com)
Practical Octokit patterntry{awaitoctokit.request("GET /repos/{owner}/{repo}",{ owner, repo });}catch(error){if(!error.status)throwerror;// not an Octokit HTTP errorconststatus=error.status;constheaders=error.response?.headers||{};constmessage=error.response?.data?.message;if(status===404){// not found OR not accessible}elseif(status===403||status===429){constremaining=headers["x-ratelimit-remaining"];constreset=headers["x-ratelimit-reset"];constretryAfter=headers["retry-after"];// decide whether this is rate limit vs permissions by checking remaining/message}else{// other HTTP errors}}Best practice: let Octokit throttle for youIf you want automatic handling/retries for rate limits, use Citations:
Handle GitHub API failures explicitly and prefer authenticated Octokit. The 🛠️ Proposed handling- const octokit = new Octokit();- const response = await octokit.rest.repos.get({- owner: parsed.data.owner,- repo: parsed.data.repo,- });+ const octokit = process.env.GITHUB_TOKEN+ ? new Octokit({ auth: process.env.GITHUB_TOKEN })+ : new Octokit();++ let response;+ try {+ response = await octokit.rest.repos.get({+ owner: parsed.data.owner,+ repo: parsed.data.repo,+ });+ } catch (err: any) {+ const status = err?.status;+ if (status === 404) {+ res.status(404).json({ error: 'GitHub repo not found' });+ return;+ }+ if (status === 401 || status === 403) {+ res.status(403).json({ error: 'GitHub access denied' });+ return;+ }+ if (status === 429) {+ res.status(429).json({ error: 'GitHub rate limit exceeded' });+ return;+ }+ logger.error({ err }, 'GitHub API request failed');+ res.status(502).json({ error: 'GitHub API error' });+ return;+ }🤖 Prompt for AI Agents | ||||||||||||||||||
| } | ||||||||||||||||||
| public async dispose() { | ||||||||||||||||||
| return new Promise<void>((resolve, reject) => { | ||||||||||||||||||
| this.server.close((err) => { | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Repo" ADD COLUMN "isAutoCleanupDisabled" BOOLEAN NOT NULL DEFAULT false; | ||
brendan-kellam marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import 'server-only'; | ||
| import { sew } from '@/actions'; | ||
| import { notFound, ServiceError } from '@/lib/serviceError'; | ||
| import { withOptionalAuthV2 } from '@/withAuthV2'; | ||
| import { RepoInfo } from './types'; | ||
| export const getRepoInfo = async (repoId: number): Promise<RepoInfo | ServiceError> => sew(() => | ||
| withOptionalAuthV2(async ({ prisma }) => { | ||
| const repo = await prisma.repo.findUnique({ | ||
| where: { id: repoId }, | ||
| include: { | ||
| jobs: { | ||
| orderBy: { | ||
| createdAt: 'desc', | ||
| }, | ||
| take: 1, | ||
| }, | ||
| }, | ||
| }); | ||
| if (!repo) { | ||
| return notFound(); | ||
| } | ||
| return { | ||
| id: repo.id, | ||
| name: repo.name, | ||
| displayName: repo.displayName, | ||
| imageUrl: repo.imageUrl, | ||
| isIndexed: repo.indexedAt !== null, | ||
| }; | ||
| }) | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.