-
Notifications
You must be signed in to change notification settings - Fork 240
chore: add GitHub repo diagnostics #543
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -51,7 +51,13 @@ async function refreshReposCache(env: Env, traceId?: string): Promise<void> { | |
| logger.info("Repo fetch completed", { | ||
| trace_id: traceId, | ||
| total_repos: repos.length, | ||
| sample_repos: repos.slice(0, 5).map((repo) => repo.fullName), | ||
| }); | ||
| if (repos.length === 0) { | ||
| logger.warn("Repo fetch returned no repositories", { | ||
| trace_id: traceId, | ||
| }); | ||
| } | ||
|
Comment on lines
51
to
+60
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. Avoid duplicating private repo and branch names in route logs. The new 🔒 Proposed safer fields- sample_repos: repos.slice(0, 5).map((repo) => repo.fullName),
+ sample_repo_count: Math.min(repos.length, 5),
@@
- sample_repos: cached.repos.slice(0, 5).map((repo) => repo.fullName),
+ sample_repo_count: Math.min(cached.repos.length, 5),
@@
- sample_repos: repos.slice(0, 5).map((repo) => repo.fullName),
+ sample_repo_count: Math.min(repos.length, 5),
@@
- sample_repos: enrichedRepos.slice(0, 5).map((repo) => repo.fullName),
+ sample_repo_count: Math.min(enrichedRepos.length, 5),
@@
- sample_branches: branches.slice(0, 10).map((branch) => branch.name),
+ sample_branch_count: Math.min(branches.length, 10),Also applies to: 145-158, 192-201, 236-247, 352-365 🤖 Prompt for AI Agents |
||
| } catch (e) { | ||
| if (e instanceof SourceControlProviderError && e.errorType === "permanent" && !e.httpStatus) { | ||
| logger.warn("SCM provider not configured, skipping repo refresh", { | ||
|
|
@@ -136,6 +142,21 @@ async function handleListRepos( | |
| if (cached) { | ||
| const isFresh = cached.freshUntil && Date.now() < cached.freshUntil; | ||
|
|
||
| logger.info("Serving repos cache", { | ||
| trace_id: ctx.trace_id, | ||
| cache_state: isFresh ? "fresh" : "stale", | ||
| cached_at: cached.cachedAt, | ||
| repo_count: cached.repos.length, | ||
| sample_repos: cached.repos.slice(0, 5).map((repo) => repo.fullName), | ||
| }); | ||
| if (cached.repos.length === 0) { | ||
| logger.warn("Repos cache returned no repositories", { | ||
| trace_id: ctx.trace_id, | ||
| cache_state: isFresh ? "fresh" : "stale", | ||
| cached_at: cached.cachedAt, | ||
| }); | ||
| } | ||
|
|
||
| if (!isFresh && ctx.executionCtx) { | ||
| // Stale — serve immediately but refresh in background | ||
| logger.info("Serving stale repos cache, refreshing in background", { | ||
|
|
@@ -171,7 +192,13 @@ async function handleListRepos( | |
| logger.info("Repo fetch completed", { | ||
| trace_id: ctx.trace_id, | ||
| total_repos: repos.length, | ||
| sample_repos: repos.slice(0, 5).map((repo) => repo.fullName), | ||
| }); | ||
| if (repos.length === 0) { | ||
| logger.warn("Repo fetch returned no repositories", { | ||
| trace_id: ctx.trace_id, | ||
| }); | ||
| } | ||
|
|
||
| const metadataStore = new RepoMetadataStore(env.DB); | ||
| let metadataMap: Map<string, RepoMetadata>; | ||
|
|
@@ -206,6 +233,19 @@ async function handleListRepos( | |
| logger.warn("Failed to cache repos list", { error: e instanceof Error ? e : String(e) }); | ||
| } | ||
|
|
||
| logger.info("Returning repos response", { | ||
| trace_id: ctx.trace_id, | ||
| cached: false, | ||
| repo_count: enrichedRepos.length, | ||
| sample_repos: enrichedRepos.slice(0, 5).map((repo) => repo.fullName), | ||
| }); | ||
| if (enrichedRepos.length === 0) { | ||
| logger.warn("Repos response contains no repositories", { | ||
| trace_id: ctx.trace_id, | ||
| cached: false, | ||
| }); | ||
| } | ||
|
|
||
| return json({ | ||
| repos: enrichedRepos, | ||
| cached: false, | ||
|
|
@@ -300,7 +340,7 @@ async function handleListBranches( | |
| _request: Request, | ||
| env: Env, | ||
| match: RegExpMatchArray, | ||
| _ctx: RequestContext | ||
| ctx: RequestContext | ||
| ): Promise<Response> { | ||
| const params = extractRepoParams(match); | ||
| if (params instanceof Response) return params; | ||
|
|
@@ -309,6 +349,20 @@ async function handleListBranches( | |
| try { | ||
| const provider = createRouteSourceControlProvider(env); | ||
| const branches = await provider.listBranches({ owner, name }); | ||
| logger.info("Branches fetched", { | ||
| trace_id: ctx.trace_id, | ||
| repo_owner: owner, | ||
| repo_name: name, | ||
| branch_count: branches.length, | ||
| sample_branches: branches.slice(0, 10).map((branch) => branch.name), | ||
| }); | ||
| if (branches.length === 0) { | ||
| logger.warn("Branch fetch returned no branches", { | ||
| trace_id: ctx.trace_id, | ||
| repo_owner: owner, | ||
| repo_name: name, | ||
| }); | ||
| } | ||
| return json({ branches }); | ||
| } catch (e) { | ||
| if (e instanceof SourceControlProviderError && e.errorType === "permanent" && !e.httpStatus) { | ||
|
|
@@ -318,6 +372,7 @@ async function handleListBranches( | |
| error: e instanceof Error ? e : String(e), | ||
| repo_owner: owner, | ||
| repo_name: name, | ||
| trace_id: ctx.trace_id, | ||
| }); | ||
| return error("Failed to list branches", 500); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,19 +10,61 @@ export async function GET( | |
| ) { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user) { | ||
| console.warn("[branches] unauthorized branch list request"); | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { owner, name } = await params; | ||
| const requester = session.user.login ?? session.user.email ?? "unknown"; | ||
|
|
||
| console.info("[branches] fetching branches from control plane", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| }); | ||
|
Comment on lines
+18
to
+23
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. Redact requester and branch-name samples from web logs.
🔒 Proposed safer payloads- const requester = session.user.login ?? session.user.email ?? "unknown";
+ const requesterType = session.user.login ? "github_login" : session.user.email ? "email" : "unknown";
@@
- requester,
+ requesterType,
repo: `${owner}/${name}`,
@@
- requester,
+ requesterType,
repo: `${owner}/${name}`,
branchCount: branches.length,
- sampleBranches: branches.slice(0, 10).map((branch) => branch.name),
+ sampleBranchCount: Math.min(branches.length, 10),
@@
- requester,
+ requesterType,
repo: `${owner}/${name}`,
@@
- requester,
+ requesterType,
repo: `${owner}/${name}`,Also applies to: 41-59 🤖 Prompt for AI Agents |
||
|
|
||
| try { | ||
| const response = await controlPlaneFetch( | ||
| `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches` | ||
| ); | ||
| console.info("[branches] control plane response received", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| status: response.status, | ||
| ok: response.ok, | ||
| }); | ||
| const data = await response.json(); | ||
| if (response.ok) { | ||
| const branches = | ||
| typeof data === "object" && data !== null && "branches" in data | ||
| ? ((data as { branches?: Array<{ name: string }> }).branches ?? []) | ||
| : []; | ||
| console.info("[branches] branches loaded", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| branchCount: branches.length, | ||
| sampleBranches: branches.slice(0, 10).map((branch) => branch.name), | ||
| }); | ||
| if (branches.length === 0) { | ||
| console.warn("[branches] branch list is empty", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| }); | ||
| } | ||
| } else { | ||
| console.error("[branches] control plane API error", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| status: response.status, | ||
| data, | ||
| }); | ||
| } | ||
| return NextResponse.json(data, { status: response.status }); | ||
| } catch (error) { | ||
| console.error("Failed to fetch branches:", error); | ||
| console.error("[branches] unexpected error fetching branches", { | ||
| requester, | ||
| repo: `${owner}/${name}`, | ||
| error, | ||
| }); | ||
| return NextResponse.json({ error: "Failed to fetch branches" }, { status: 500 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,17 +13,34 @@ interface ControlPlaneReposResponse { | |
| export async function GET() { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session) { | ||
| console.warn("[repos] unauthorized repository list request"); | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const requester = session.user?.login ?? session.user?.email ?? "unknown"; | ||
|
|
||
| console.info("[repos] fetching repositories from control plane", { | ||
| requester, | ||
| }); | ||
|
Comment on lines
+20
to
+24
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. Avoid logging requester identifiers and private repo samples.
🔒 Proposed safer payloads- const requester = session.user?.login ?? session.user?.email ?? "unknown";
+ const requesterType = session.user?.login ? "github_login" : session.user?.email ? "email" : "unknown";
@@
- requester,
+ requesterType,
@@
- requester,
+ requesterType,
repoCount: data.repos.length,
cached: data.cached,
cachedAt: data.cachedAt,
- sampleRepos: data.repos.slice(0, 5).map((repo) => repo.fullName),
+ sampleRepoCount: Math.min(data.repos.length, 5),
@@
- requester,
+ requesterType,
cached: data.cached,Also applies to: 52-65 🤖 Prompt for AI Agents |
||
|
|
||
| try { | ||
| // Fetch repositories from control plane using GitHub App installation token. | ||
| // This ensures we only show repos the App has access to, not all repos the user can see. | ||
| const response = await controlPlaneFetch("/repos"); | ||
|
|
||
| console.info("[repos] control plane response received", { | ||
| requester, | ||
| status: response.status, | ||
| ok: response.ok, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const error = await response.text(); | ||
| console.error("Control plane API error:", error); | ||
| console.error("[repos] control plane API error", { | ||
| requester, | ||
| status: response.status, | ||
| error, | ||
| }); | ||
| return NextResponse.json( | ||
| { error: "Failed to fetch repositories" }, | ||
| { status: response.status } | ||
|
|
@@ -32,10 +49,28 @@ export async function GET() { | |
|
|
||
| const data: ControlPlaneReposResponse = await response.json(); | ||
|
|
||
| console.info("[repos] repositories loaded", { | ||
| requester, | ||
| repoCount: data.repos.length, | ||
| cached: data.cached, | ||
| cachedAt: data.cachedAt, | ||
| sampleRepos: data.repos.slice(0, 5).map((repo) => repo.fullName), | ||
| }); | ||
| if (data.repos.length === 0) { | ||
| console.warn("[repos] repository list is empty", { | ||
| requester, | ||
| cached: data.cached, | ||
| cachedAt: data.cachedAt, | ||
| }); | ||
| } | ||
|
|
||
| // The control plane returns repos in the format we need | ||
| return NextResponse.json({ repos: data.repos }); | ||
| } catch (error) { | ||
| console.error("Error fetching repos:", error); | ||
| console.error("[repos] unexpected error fetching repositories", { | ||
| requester, | ||
| error, | ||
| }); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,12 +47,40 @@ export const authOptions: NextAuthOptions = { | |
| }; | ||
|
|
||
| const githubProfile = profile as { login?: string }; | ||
| const githubUsername = githubProfile.login?.trim().toLowerCase(); | ||
| const emailDomain = user.email?.split("@")[1]?.trim().toLowerCase(); | ||
| const usernameMatch = githubUsername ? config.allowedUsers.includes(githubUsername) : false; | ||
| const domainMatch = emailDomain ? config.allowedDomains.includes(emailDomain) : false; | ||
| const allowAllMode = | ||
| config.allowedDomains.length === 0 && | ||
| config.allowedUsers.length === 0 && | ||
| config.unsafeAllowAllUsers; | ||
|
|
||
| const isAllowed = checkAccessAllowed(config, { | ||
| githubUsername: githubProfile.login, | ||
| email: user.email ?? undefined, | ||
| }); | ||
|
|
||
| console.info("[auth] signIn access check", { | ||
| githubUsername: githubUsername ?? null, | ||
| emailDomain: emailDomain ?? null, | ||
| allowedUsers: config.allowedUsers, | ||
| allowedDomains: config.allowedDomains, | ||
| unsafeAllowAllUsers: config.unsafeAllowAllUsers, | ||
| usernameMatch, | ||
| domainMatch, | ||
| allowAllMode, | ||
| isAllowed, | ||
| }); | ||
|
|
||
| if (!isAllowed) { | ||
| console.warn("[auth] signIn denied", { | ||
| githubUsername: githubUsername ?? null, | ||
| emailDomain: emailDomain ?? null, | ||
| allowedUsers: config.allowedUsers, | ||
| allowedDomains: config.allowedDomains, | ||
| unsafeAllowAllUsers: config.unsafeAllowAllUsers, | ||
| }); | ||
|
Comment on lines
+64
to
+83
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. Avoid logging user identifiers and allowlist contents. These logs emit 🔒 Proposed safer log payload console.info("[auth] signIn access check", {
- githubUsername: githubUsername ?? null,
- emailDomain: emailDomain ?? null,
- allowedUsers: config.allowedUsers,
- allowedDomains: config.allowedDomains,
+ hasGithubUsername: Boolean(githubUsername),
+ hasEmailDomain: Boolean(emailDomain),
+ allowedUserCount: config.allowedUsers.length,
+ allowedDomainCount: config.allowedDomains.length,
unsafeAllowAllUsers: config.unsafeAllowAllUsers,
usernameMatch,
domainMatch,
@@
if (!isAllowed) {
console.warn("[auth] signIn denied", {
- githubUsername: githubUsername ?? null,
- emailDomain: emailDomain ?? null,
- allowedUsers: config.allowedUsers,
- allowedDomains: config.allowedDomains,
+ hasGithubUsername: Boolean(githubUsername),
+ hasEmailDomain: Boolean(emailDomain),
+ allowedUserCount: config.allowedUsers.length,
+ allowedDomainCount: config.allowedDomains.length,
unsafeAllowAllUsers: config.unsafeAllowAllUsers,
});🤖 Prompt for AI Agents |
||
| return false; | ||
| } | ||
| return true; | ||
|
|
||
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.
Do not log private repo or branch names by default.
sample_reposandsample_branchescan contain confidential names from private GitHub installations. Counts/timings are useful without exposing names; gate samples behind an explicit debug flag or emit hashes/counts only.🔒 Proposed safer diagnostics
Also applies to: 646-655
🤖 Prompt for AI Agents