From ac3942e3e64ebab45f8f9f47a2b092d33c75770a Mon Sep 17 00:00:00 2001 From: Issue To PR agent Date: Mon, 15 Dec 2025 01:28:19 +0000 Subject: [PATCH 1/3] Remove global token store and refactor token handling\n\n- Deleted deprecated shared/src/auth.ts and removed all usages\n- Refactored shared GitHub utilities to use repo-scoped installation auth:\n - usecases/workflows/autoResolveIssue now derives installation token and\n passes it through; no global state\n - lib/utils/utils-server now builds clone URL via installation token\n - lib/github/refs uses installation GraphQL client\n - lib/github/issues and lib/github/pullRequests use installation-scoped\n Octokit/GraphQL per repo\n - Stubbed deprecated shared/lib/github getOctokit/getGraphQLClient\n to enforce token injection pattern\n - Marked shared GraphQL listUserRepositories as unavailable in shared\n- Updated worker orchestrator to stop setting a global access token\n\nThis removes concurrency-unsafe global token state and injects tokens in the\nproper context (GitHub App installation per repository). --- .../src/orchestrators/autoResolveIssue.ts | 14 +-- shared/src/auth.ts | 35 ------- .../graphql/queries/listUserRepositories.ts | 76 ++------------- shared/src/lib/github/index.ts | 67 ++----------- shared/src/lib/github/issues.ts | 76 +++++++-------- shared/src/lib/github/pullRequests.ts | 93 +++++++------------ shared/src/lib/github/refs.ts | 11 ++- shared/src/lib/utils/utils-server.ts | 43 ++------- .../usecases/workflows/autoResolveIssue.ts | 13 +-- 9 files changed, 104 insertions(+), 324 deletions(-) delete mode 100644 shared/src/auth.ts diff --git a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts index 739c4f473..99780b1f2 100644 --- a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts +++ b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts @@ -4,7 +4,6 @@ import type { Transaction } from "neo4j-driver" import { EventBusAdapter } from "shared/adapters/ioredis/EventBusAdapter" import { createNeo4jDataSource } from "shared/adapters/neo4j/dataSource" import { makeSettingsReaderAdapter } from "shared/adapters/neo4j/repositories/SettingsReaderAdapter" -import { setAccessToken } from "shared/auth" import { getPrivateKeyFromFile } from "shared/services/fs" import { autoResolveIssue as autoResolveIssueWorkflow } from "shared/usecases/workflows/autoResolveIssue" @@ -82,7 +81,7 @@ export async function autoResolveIssue( const privateKey = await getPrivateKeyFromFile(GITHUB_APP_PRIVATE_KEY_PATH) // GitHub API via App Installation - // Temporary: set the access token for the workflow + // Installation-scoped Octokit (if needed locally) const octokit = new Octokit({ authStrategy: createAppAuth, auth: { @@ -94,16 +93,6 @@ export async function autoResolveIssue( // Setup adapters for event bus const eventBusAdapter = new EventBusAdapter(REDIS_URL) - const auth = await octokit.auth({ type: "installation" }) - if ( - !auth || - typeof auth !== "object" || - !("token" in auth) || - typeof auth.token !== "string" - ) { - throw new Error("Failed to get installation token") - } - setAccessToken(auth.token) await publishJobStatus(jobId, "Fetching issue and running LLM") @@ -123,3 +112,4 @@ export async function autoResolveIssue( // Handler will publish the completion status return result.messages } + diff --git a/shared/src/auth.ts b/shared/src/auth.ts deleted file mode 100644 index 71e7b9aaa..000000000 --- a/shared/src/auth.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Minimal, process-local token store for quick migration. -// Not concurrency-safe across multiple parallel workflows. -// For isolation, consider the scoped variant below. -// Super temporary! Refactor away as soon as possible. - -let ACCESS_TOKEN: string | null = null - -/** - * @deprecated Refactor away from this ASAP. Use clean code architecture to provide access tokens to adapters instead. - */ -export function setAccessToken(token: string) { - ACCESS_TOKEN = typeof token === "string" ? token.trim() : null -} - -/** - * @deprecated Refactor away from this ASAP. Use clean code architecture to provide access tokens to adapters instead. - */ -export function getAccessToken(): string | null { - return ACCESS_TOKEN -} - -/** - * @deprecated Refactor away from this ASAP. Use clean code architecture to provide access tokens to adapters instead. - */ -export function getAccessTokenOrThrow(): string { - if (!ACCESS_TOKEN) throw new Error("No access token set") - return ACCESS_TOKEN -} - -/** - * @deprecated Refactor away from this ASAP. Use clean code architecture to provide access tokens to adapters instead. - */ -export function clearAccessToken() { - ACCESS_TOKEN = null -} diff --git a/shared/src/lib/github/graphql/queries/listUserRepositories.ts b/shared/src/lib/github/graphql/queries/listUserRepositories.ts index 7ee4cb22c..e980f36d9 100644 --- a/shared/src/lib/github/graphql/queries/listUserRepositories.ts +++ b/shared/src/lib/github/graphql/queries/listUserRepositories.ts @@ -1,75 +1,13 @@ "use server" -import { withTiming } from "shared/utils/telemetry" -import { z } from "zod" - -import { getUserOctokit } from "@/lib/github" - /** - * GraphQL document for listing the current viewer's repositories ordered by last update. - * Keep the Zod schemas below in sync with this selection set whenever you edit it. + * This function is not available in the shared package because it requires + * a user session token (NextAuth). Use the app-layer implementation instead: + * `lib/github/graphql/queries/listUserRepositories.ts`. */ -const LIST_USER_REPOSITORIES_QUERY = ` - query ListUserRepositories { - viewer { - repositories(first: 50, orderBy: { field: UPDATED_AT, direction: DESC }) { - nodes { - name - nameWithOwner - description - updatedAt - } - } - } - } -` as const - -const RepoSelectorItemSchema = z.object({ - name: z.string(), - nameWithOwner: z.string(), - description: z.string().nullable(), - updatedAt: z.string(), -}) -type RepoSelectorItem = z.infer - -const ResponseSchema = z.object({ - viewer: z.object({ - repositories: z.object({ - nodes: z.array(RepoSelectorItemSchema), - }), - }), -}) - -type ListUserRepositoriesResponse = z.infer - -/** - * - * Lists repositories that are available to the user AND the Github App - * We use `getUserOctokit` to retreive the list of repositories that are visible - * to both the user AND the Github App. - * This is different from `listUserAppRepositories` which only lists repositories - * that have the Github App installed. - * Therefore, public repositories will be included in this list, even if they - * don't have the Github App installed. Since the Github App can also "see" public repositories. - */ -export async function listUserRepositories(): Promise { - const octokit = await getUserOctokit() - const graphqlWithAuth = octokit.graphql - - if (!graphqlWithAuth) { - throw new Error("Could not initialize GraphQL client") - } - - const data = await withTiming("GitHub GraphQL: listUserRepositories", () => - graphqlWithAuth(LIST_USER_REPOSITORIES_QUERY) +export async function listUserRepositories(): Promise { + throw new Error( + "shared: listUserRepositories is not available. Use the app-layer version that has access to user session." ) - - const parsed = ResponseSchema.parse(data) - - return parsed.viewer.repositories.nodes.map((node) => ({ - nameWithOwner: node.nameWithOwner, - name: node.name, - description: node.description, - updatedAt: node.updatedAt, - })) } + diff --git a/shared/src/lib/github/index.ts b/shared/src/lib/github/index.ts index 0df039f21..8609c0e52 100644 --- a/shared/src/lib/github/index.ts +++ b/shared/src/lib/github/index.ts @@ -1,13 +1,11 @@ "use server" import { createAppAuth } from "@octokit/auth-app" -import { createOAuthUserAuth } from "@octokit/auth-oauth-user" import { graphql } from "@octokit/graphql" import { Octokit } from "@octokit/rest" import * as fs from "fs/promises" import { App } from "octokit" -import { getAccessTokenOrThrow } from "@/auth" import { ExtendedOctokit } from "@/lib/types/github" export async function getPrivateKeyFromFile(): Promise { @@ -19,34 +17,21 @@ export async function getPrivateKeyFromFile(): Promise { } /** - * Creates an authenticated Octokit client using one of two authentication methods: - * 1. User Authentication: Tries to use the user's session token first - * 2. GitHub App Authentication: Falls back to using GitHub App credentials (private key + app ID) - * if user authentication fails - * - * Returns either an authenticated Octokit instance or null if both auth methods fail - * - * @deprecated Use getUserOctokit or getInstallationOctokit instead + * @deprecated Not available in shared package. Use repo-scoped installation clients instead. */ export default async function getOctokit(): Promise { - const token = getAccessTokenOrThrow() - - const userOctokit = new Octokit({ auth: token }) - - return { ...userOctokit, authType: "user" } + throw new Error( + "shared/lib/github:getOctokit is deprecated. Use installation-scoped clients (getInstallationOctokit) at the call site." + ) } /** - * Creates an authenticated GraphQL client using Github App Authentication + * @deprecated Not available in shared package. Use installation-scoped clients instead. */ export async function getGraphQLClient(): Promise { - const octokit = await getOctokit() - - if (!octokit) { - return null - } - - return octokit.graphql + throw new Error( + "shared/lib/github:getGraphQLClient is deprecated. Use installationOctokit.graphql for a specific repo." + ) } // TODO: Get rid of @@ -74,41 +59,8 @@ export async function getTestInstallationOctokit( } /** - * Creates an authenticated Octokit client using the OAuth user authentication strategy. - * This function uses the existing session tokens from NextAuth to authenticate with GitHub. - * - * This is an alternative to getUserOctokit() that uses the @octokit/auth-oauth-user strategy - * instead of directly passing the access token to the Octokit constructor. - * - * @returns An authenticated Octokit instance or throws an error if authentication fails + * Creates an authenticated Octokit client for a GitHub App installation. */ -export async function getUserOctokit(): Promise { - const token = getAccessTokenOrThrow() - - // `clientId` and `clientSecret` are already determined by - // auth.js library when authenticating user in `auth.js`. - // No need to add them here, as they're inferred in the `access_token` - const userOctokit = new Octokit({ - authStrategy: createOAuthUserAuth, - auth: { - clientType: "github-app", - token, - }, - }) - - return userOctokit -} - -export async function getUserInstallations() { - const octokit = await getUserOctokit() - - const { data: installations } = await octokit.request( - "GET /user/installations" - ) - - return installations.installations -} - export async function getInstallationOctokit( installationId: number ): Promise { @@ -136,3 +88,4 @@ export async function getAppOctokit(): Promise { }) return app } + diff --git a/shared/src/lib/github/issues.ts b/shared/src/lib/github/issues.ts index 714650868..11390c60c 100644 --- a/shared/src/lib/github/issues.ts +++ b/shared/src/lib/github/issues.ts @@ -1,6 +1,6 @@ import { withTiming } from "shared/utils/telemetry" -import getOctokit, { getGraphQLClient, getUserOctokit } from "@/lib/github" +import { getInstallationFromRepo, getInstallationOctokit } from "@/lib/github" import { getLatestPlanIdsForIssues, getPlanStatusForIssues, @@ -13,27 +13,30 @@ import { ListForRepoParams, } from "@/lib/types/github" -type CreateIssueParams = { - repo: string - owner: string - title: string - body: string +// Helper: installation-scoped Octokit for a repoFullName +async function getInstallationOctokitForRepo(repoFullName: string) { + const [owner, repo] = repoFullName.split("/") + if (!owner || !repo) { + throw new Error("Invalid repository format. Expected 'owner/repo'") + } + const installation = await getInstallationFromRepo({ owner, repo }) + return getInstallationOctokit(installation.data.id) } -// Derive the exact response type from the installed Octokit method -type UserOctokit = Awaited> -type CreateIssueResponse = Awaited< - ReturnType -> - +// Create Issue using installation auth (requires appropriate app permissions) export async function createIssue({ repo, owner, title, body, -}: CreateIssueParams): Promise { - const octokit = await getUserOctokit() - if (!octokit) throw new Error("No octokit found") +}: { + repo: string + owner: string + title: string + body: string +}) { + const repoFullName = `${owner}/${repo}` + const octokit = await getInstallationOctokitForRepo(repoFullName) return await octokit.rest.issues.create({ owner, repo, title, body }) } @@ -45,15 +48,12 @@ export async function getIssue({ fullName: string issueNumber: number }): Promise { - const octokit = await getOctokit() - if (!octokit) { - return { type: "other_error", error: "No octokit found" } - } - const [owner, repo] = fullName.split("/") - if (!owner || !repo) { - return { type: "not_found" } - } try { + const octokit = await getInstallationOctokitForRepo(fullName) + const [owner, repo] = fullName.split("/") + if (!owner || !repo) { + return { type: "not_found" } + } const issue = await octokit.rest.issues.get({ owner, repo, @@ -86,11 +86,8 @@ export async function createIssueComment({ repoFullName: string comment: string }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } const issue = await octokit.rest.issues.createComment({ owner, repo, @@ -107,14 +104,11 @@ export async function getIssueComments({ repoFullName: string issueNumber: number }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") if (!owner || !repo) { throw new Error("Invalid repository format. Expected 'owner/repo'") } - if (!octokit) { - throw new Error("No octokit found") - } const comments = await octokit.rest.issues.listComments({ owner, repo, @@ -129,13 +123,9 @@ export async function getIssueList({ }: { repoFullName: string } & Omit): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } - const issuesResponse = await withTiming( `GitHub REST: issues.listForRepo ${repoFullName}`, async () => @@ -158,11 +148,8 @@ export async function updateIssueComment({ repoFullName: string comment: string }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } try { const updatedComment = await octokit.rest.issues.updateComment({ owner, @@ -259,8 +246,8 @@ export async function getLinkedPRNumberForIssue({ issueNumber: number }): Promise { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql const query = ` query($owner: String!, $repo: String!, $issueNumber: Int!) { @@ -369,8 +356,8 @@ export async function getLinkedPRNumbersForIssues({ issueNumbers: number[] }): Promise> { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql // Build a single query with field aliases, one per issueNumber // Example alias: i_123: issue(number: 123) { ... } @@ -500,3 +487,4 @@ export async function getLinkedPRNumbersForIssues({ return result } + diff --git a/shared/src/lib/github/pullRequests.ts b/shared/src/lib/github/pullRequests.ts index 7f3a8d9f0..8cc5650ba 100644 --- a/shared/src/lib/github/pullRequests.ts +++ b/shared/src/lib/github/pullRequests.ts @@ -1,6 +1,6 @@ import { logEnd, logStart, withTiming } from "shared/utils/telemetry" -import getOctokit, { getGraphQLClient } from "@/lib/github" +import { getInstallationFromRepo, getInstallationOctokit } from "@/lib/github" import { IssueComment, PullRequest, @@ -8,6 +8,15 @@ import { PullRequestReview, } from "@/lib/types/github" +async function getInstallationOctokitForRepo(repoFullName: string) { + const [owner, repo] = repoFullName.split("/") + if (!owner || !repo) { + throw new Error("Invalid repository format. Expected 'owner/repo'") + } + const installation = await getInstallationFromRepo({ owner, repo }) + return getInstallationOctokit(installation.data.id) +} + export async function getPullRequestOnBranch({ repoFullName, branch, @@ -15,15 +24,8 @@ export async function getPullRequestOnBranch({ repoFullName: string branch: string }) { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!owner || !repo) { - throw new Error("Invalid repository format. Expected 'owner/repo'") - } - - if (!octokit) { - throw new Error("No octokit found") - } const pr = await withTiming( `GitHub REST: pulls.list head=${owner}:${branch}`, @@ -82,10 +84,8 @@ export async function createPullRequest({ throw new Error("Invalid repository format. Expected 'owner/repo'") } - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) { - throw new Error("Could not initialize GraphQL client") - } + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql // 1. Retrieve repository ID (required for mutation input) const repoIdQuery = ` @@ -97,8 +97,7 @@ export async function createPullRequest({ ` const repoIdResult = await withTiming( `GitHub GraphQL: get repository id ${repoFullName}`, - () => - graphqlWithAuth(repoIdQuery, { owner, name: repo }) + () => graphqlWithAuth(repoIdQuery, { owner, name: repo }) ) const repositoryId = repoIdResult.repository.id @@ -166,10 +165,8 @@ export async function createPullRequestToBase({ throw new Error("Invalid repository format. Expected 'owner/repo'") } - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) { - throw new Error("Could not initialize GraphQL client") - } + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql const repoIdQuery = ` query ($owner: String!, $name: String!) { @@ -180,8 +177,7 @@ export async function createPullRequestToBase({ ` const repoIdResult = await withTiming( `GitHub GraphQL: get repository id ${repoFullName}`, - () => - graphqlWithAuth(repoIdQuery, { owner, name: repo }) + () => graphqlWithAuth(repoIdQuery, { owner, name: repo }) ) const repositoryId = repoIdResult.repository.id if (!repositoryId) throw new Error("Failed to retrieve repository ID") @@ -227,11 +223,7 @@ export async function getPullRequestDiff({ pullNumber: number }): Promise { try { - const octokit = await getOctokit() - if (!octokit) { - throw new Error("No octokit found") - } - + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") const response = await withTiming( @@ -266,11 +258,7 @@ export async function getPullRequestList({ }: { repoFullName: string }): Promise { - const octokit = await getOctokit() - if (!octokit) { - throw new Error("No octokit found") - } - + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") const pullRequests = await withTiming( @@ -292,13 +280,9 @@ export async function getPullRequestComments({ repoFullName: string pullNumber: number }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } - const commentsResponse = await withTiming( `GitHub REST: issues.listComments PR ${repoFullName}#${pullNumber}`, () => @@ -319,13 +303,9 @@ export async function getPullRequestReviews({ repoFullName: string pullNumber: number }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } - const reviewsResponse = await withTiming( `GitHub REST: pulls.listReviews ${repoFullName}#${pullNumber}`, () => @@ -382,8 +362,8 @@ export async function getPullRequestReviewCommentsGraphQL({ commentsPerReview?: number }) { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql // GraphQL query for reviews and review comments const query = ` query($owner: String!, $repo: String!, $pullNumber: Int!, $reviewsLimit: Int!, $commentsPerReview: Int!) { @@ -462,13 +442,9 @@ export async function getPullRequest({ repoFullName: string pullNumber: number }): Promise { - const octokit = await getOctokit() + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!octokit) { - throw new Error("No octokit found") - } - const response = await withTiming( `GitHub REST: pulls.get ${repoFullName}#${pullNumber}`, () => @@ -491,14 +467,8 @@ export async function addLabelsToPullRequest({ pullNumber: number labels: string[] }): Promise { - const octokit = await getOctokit() - if (!octokit) { - throw new Error("No octokit found") - } + const octokit = await getInstallationOctokitForRepo(repoFullName) const [owner, repo] = repoFullName.split("/") - if (!owner || !repo) { - throw new Error("Invalid repository format. Expected 'owner/repo'") - } await withTiming( `GitHub REST: issues.addLabels ${repoFullName}#${pullNumber}`, () => @@ -533,8 +503,8 @@ export async function getPRLinkedIssuesMap( repoFullName: string ): Promise> { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql let hasNextPage = true let endCursor: string | null = null @@ -598,8 +568,8 @@ export async function getIssueToPullRequestMap( repoFullName: string ): Promise> { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql let hasNextPage = true let endCursor: string | null = null @@ -669,8 +639,8 @@ export async function getLinkedIssuesForPR({ pullNumber: number }): Promise { const [owner, repo] = repoFullName.split("/") - const graphqlWithAuth = await getGraphQLClient() - if (!graphqlWithAuth) throw new Error("Could not initialize GraphQL client") + const octokit = await getInstallationOctokitForRepo(repoFullName) + const graphqlWithAuth = octokit.graphql const query = ` query($owner: String!, $repo: String!, $pullNumber: Int!) { @@ -703,3 +673,4 @@ export async function getLinkedIssuesForPR({ response.repository?.pullRequest?.closingIssuesReferences?.nodes || [] return nodes.map((n) => n.number) } + diff --git a/shared/src/lib/github/refs.ts b/shared/src/lib/github/refs.ts index 862b3b600..06561ed7d 100644 --- a/shared/src/lib/github/refs.ts +++ b/shared/src/lib/github/refs.ts @@ -1,6 +1,6 @@ "use server" -import { getGraphQLClient } from "@/lib/github" +import { getInstallationFromRepo, getInstallationOctokit } from "@/lib/github" import { RepoFullName } from "@/lib/types/github" interface BranchByCommitDate { @@ -37,10 +37,10 @@ export async function listBranchesSortedByCommitDate( ): Promise { const { owner, repo } = repoFullName - const graphql = await getGraphQLClient() - if (!graphql) { - throw new Error("No authenticated GraphQL client available") - } + // Use installation-scoped Octokit for this repository (no global token) + const installation = await getInstallationFromRepo({ owner, repo }) + const installationOctokit = await getInstallationOctokit(installation.data.id) + const graphql = installationOctokit.graphql const query = ` query ($owner: String!, $repo: String!, $pageSize: Int!, $after: String) { @@ -109,3 +109,4 @@ export async function listBranchesSortedByCommitDate( } return allBranches } + diff --git a/shared/src/lib/utils/utils-server.ts b/shared/src/lib/utils/utils-server.ts index 85dadced6..ea38eaceb 100644 --- a/shared/src/lib/utils/utils-server.ts +++ b/shared/src/lib/utils/utils-server.ts @@ -2,7 +2,6 @@ import { AsyncLocalStorage } from "node:async_hooks" -import { getAccessToken } from "@/auth" import { getLocalRepoDir } from "@/lib/fs" import { cleanCheckout, @@ -11,7 +10,7 @@ import { ensureValidRepo, setRemoteOrigin, } from "@/lib/git" -import getOctokit from "@/lib/github" +import { getInstallationTokenFromRepo } from "@/lib/github/installation" import { getCloneUrlWithAccessToken } from "@/lib/utils/utils-common" // For storing Github App installation ID in async context @@ -44,9 +43,7 @@ export function getInstallationId(): string | null { * available in a local working directory that the server can freely mutate. * The steps performed are: * 1. Resolve (and lazily create) the base directory via `getLocalRepoDir`. - * 2. Build an authenticated clone URL using either the user's - * GitHub App token (OAuth or installation token) exposed - * through `runWithInstallationId` / `getInstallationId`. + * 2. Build an authenticated clone URL using the repository's GitHub App installation token. * 3. Verify that the local repository is healthy via `ensureValidRepo`; if it * is corrupt or missing, attempt a fresh clone. * 4. Ensure the local repo's "origin" remote uses the authenticated URL so @@ -79,34 +76,13 @@ export async function setupLocalRepository({ try { let cloneUrl: string - // 1. Determine an authenticated clone URL - const token = getAccessToken() - if (token) { - cloneUrl = getCloneUrlWithAccessToken(repoFullName, token) - } else { - // Fallback to GitHub App authentication - const octokit = await getOctokit() - if (!octokit) { - throw new Error("Failed to get authenticated Octokit instance") - } - - const [owner, repo] = repoFullName.split("/") - const { data: repoData } = await octokit.rest.repos.get({ - owner, - repo, - }) - - cloneUrl = repoData.clone_url as string - - const installationId = getInstallationId() - if (installationId) { - const token = (await octokit.auth({ - type: "installation", - installationId: Number(installationId), - })) as { token: string } - cloneUrl = getCloneUrlWithAccessToken(repoFullName, token.token) - } - } + // 1. Determine an authenticated clone URL using the installation token + const [owner, repo] = repoFullName.split("/") + const installationToken = await getInstallationTokenFromRepo({ + owner, + repo, + }) + cloneUrl = getCloneUrlWithAccessToken(repoFullName, installationToken) // 2. Ensure repository exists and is healthy await ensureValidRepo(baseDir, cloneUrl) @@ -149,3 +125,4 @@ export async function setupLocalRepository({ throw error } } + diff --git a/shared/src/usecases/workflows/autoResolveIssue.ts b/shared/src/usecases/workflows/autoResolveIssue.ts index 91c202318..5e6133c0f 100644 --- a/shared/src/usecases/workflows/autoResolveIssue.ts +++ b/shared/src/usecases/workflows/autoResolveIssue.ts @@ -8,7 +8,6 @@ import { v4 as uuidv4 } from "uuid" import GitHubRefsAdapter from "@/adapters/github/GitHubRefsAdapter" import { OpenAIAdapter } from "@/adapters/llm/OpenAIAdapter" -import { getAccessTokenOrThrow } from "@/auth" import PlanAndCodeAgent from "@/lib/agents/PlanAndCodeAgent" import { getInstallationTokenFromRepo } from "@/lib/github/installation" import { getIssue, getIssueComments } from "@/lib/github/issues" @@ -84,8 +83,10 @@ export const autoResolveIssue = async ( throw new Error(`Failed to fetch issue #${issueNumber}, ${repoFullName}`) } const issue = issueResult.issue - const access_token = getAccessTokenOrThrow() - const octokit = new Octokit({ auth: access_token }) + + // Use installation token tied to the repository (avoids any global token state) + const sessionToken = await getInstallationTokenFromRepo({ owner, repo }) + const octokit = new Octokit({ auth: sessionToken }) const repository = await octokit.rest.repos.get({ owner, repo }) // ================================================= @@ -168,11 +169,6 @@ export const autoResolveIssue = async ( const env: RepoEnvironment = { kind: "container", name: containerName } - const sessionToken = await getInstallationTokenFromRepo({ - owner, - repo, - }) - const trace = langfuse.trace({ name: "autoResolve" }) const span = trace.span({ name: "PlanAndCodeAgent" }) @@ -239,3 +235,4 @@ export const autoResolveIssue = async ( throw error } } + From b72eaa09a49d9a449abc4c5f76cef6c1057f503f Mon Sep 17 00:00:00 2001 From: Ching Jui Young Date: Mon, 15 Dec 2025 11:25:44 +0800 Subject: [PATCH 2/3] update note --- shared/src/lib/utils/utils-server.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/shared/src/lib/utils/utils-server.ts b/shared/src/lib/utils/utils-server.ts index ea38eaceb..7e643c241 100644 --- a/shared/src/lib/utils/utils-server.ts +++ b/shared/src/lib/utils/utils-server.ts @@ -23,11 +23,9 @@ export function runWithInstallationId( asyncLocalStorage.run({ installationId }, fn) } -// TODO: We currently depend on webhooks to provide installation IDs. -// BUT, we should also save installation IDs to neo4j database on the first time we retrieve them. -// They are unique to: -// - Our Github App (dev-issue-to-pr (local testing) or issuetopr-dev (production)) (confusing, I know) -// - user / org +// TODO: I think we should follow prescribed protocol for using the installation ID from Github. +// Usually they are provided via webhooks, or you can retrieve via their API. +// We should get rid of this function and follow the recommended approach from Github API. export function getInstallationId(): string | null { const store = asyncLocalStorage.getStore() if (!store) { @@ -74,15 +72,13 @@ export async function setupLocalRepository({ const baseDir = await getLocalRepoDir(repoFullName) try { - let cloneUrl: string - // 1. Determine an authenticated clone URL using the installation token const [owner, repo] = repoFullName.split("/") const installationToken = await getInstallationTokenFromRepo({ owner, repo, }) - cloneUrl = getCloneUrlWithAccessToken(repoFullName, installationToken) + const cloneUrl = getCloneUrlWithAccessToken(repoFullName, installationToken) // 2. Ensure repository exists and is healthy await ensureValidRepo(baseDir, cloneUrl) @@ -125,4 +121,3 @@ export async function setupLocalRepository({ throw error } } - From e88b094b031428b93c2a5cd675b054821a64fec4 Mon Sep 17 00:00:00 2001 From: Ching Jui Young Date: Mon, 15 Dec 2025 11:25:49 +0800 Subject: [PATCH 3/3] cleanup --- .../workflow-workers/src/orchestrators/autoResolveIssue.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts index 99780b1f2..d588bb967 100644 --- a/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts +++ b/apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts @@ -44,9 +44,9 @@ export async function autoResolveIssue( { repoFullName, issueNumber, + branch, githubLogin, githubInstallationId, - branch, }: AutoResolveJobData ) { await publishJobStatus( @@ -112,4 +112,3 @@ export async function autoResolveIssue( // Handler will publish the completion status return result.messages } -