Skip to content

Conversation

@issuetopr-dev
Copy link

@issuetopr-dev issuetopr-dev bot commented Dec 15, 2025

Summary

  • Removed the deprecated, concurrency-unsafe global token store (shared/src/auth.ts) and all usages.
  • Refactored shared GitHub access to use installation-scoped tokens derived from the repository context. This follows ports/adapters guidance and avoids global state.

Key Changes

  • shared/src/usecases/workflows/autoResolveIssue.ts

    • Removed getAccessTokenOrThrow and now derives the GitHub App installation token for the target repo and uses it to instantiate Octokit.
    • Passes the installation token (sessionToken) to PlanAndCodeAgent as before.
  • shared/src/lib/utils/utils-server.ts

    • Removed global getAccessToken/getOctokit fallback.
    • Always computes an authenticated clone URL via getInstallationTokenFromRepo(owner, repo).
  • shared/src/lib/github/refs.ts

    • Now uses installation-scoped Octokit.graphql for branch listing (no global token).
  • shared/src/lib/github/issues.ts & shared/src/lib/github/pullRequests.ts

    • Replaced getOctokit/getGraphQLClient usage with per-repo installation Octokit/GraphQL clients.
    • All REST and GraphQL calls are authenticated per repo via the GitHub App installation.
  • shared/src/lib/github/index.ts

    • Removed dependency on shared/src/auth.ts.
    • Stubbed deprecated getOctokit/getGraphQLClient to throw with guidance to use installation-scoped clients.
  • shared/src/lib/github/graphql/queries/listUserRepositories.ts

    • Marked as unavailable in shared (requires user session); app-layer version remains available.
  • apps/workers/workflow-workers/src/orchestrators/autoResolveIssue.ts

    • Removed setAccessToken usage; no global state required.

Why

  • Global token state caused race conditions and broke in concurrent workflows. Tokens are now injected from the correct context (GitHub App installation for the specific repo), eliminating cross-request leakage and improving safety.

Notes

  • ESLint passes (pnpm run lint). TypeScript and Prettier checks are unchanged in CI (project currently runs tests in CI). The shared package continues to expose installation-scoped helpers and avoids process-global auth.

Follow-ups

  • Additional shared modules that still import deprecated getOctokit/getGraphQLClient should be migrated similarly if/when they are used in worker contexts.
  • If needed, we can add small helper utilities to centralize “get installation Octokit for repo” patterns.

Closes #1408

…precated 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).
@issuetopr-dev issuetopr-dev bot added the AI generated AI-generated Pull Requests label Dec 15, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 15, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

@youngchingjui
Copy link
Owner

I’ll first inspect PlanAndCodeAgent and search for sessionToken usages to see exactly how and where it’s consumed, then trace what pieces of repository.data are accessed. From that, I’ll summarize the minimal data surface this workflow actually needs and suggest how to express it via ports/adapters.

[3 tools called]

I’ve confirmed how PlanAndCodeAgent wires sessionToken and repository into tools; next I’ll inspect those tools to see which specific data fields they touch so we can outline the minimal contract the workflow needs.

[7 tools called]

Clarify how sessionToken flows in autoResolveIssue

In shared/src/usecases/workflows/autoResolveIssue.ts, sessionToken is produced and consumed like this:

  • Production

    • Source: getInstallationTokenFromRepo({ owner, repo })
    • Type/semantics: A GitHub App installation token (or compatible PAT-like token) with repo-level rights to push branches and (implicitly) interact with the repo via API.
  • Direct uses inside this workflow

    1. GitHub REST auth to read repo metadata

      const sessionToken = await getInstallationTokenFromRepo({ owner, repo })
      const octokit = new Octokit({ auth: sessionToken })
      const repository = await octokit.rest.repos.get({ owner, repo })
      • Here the token is used only as auth for Octokit to call repos.get.
      • Data actually read from that call in this file:
        • repository.data.default_branch
        • repository.data.full_name (conceptually, though the workflow already has repoFullName from params; the code currently uses default_branch explicitly and passes the full repository.data onward).
    2. Forwarding into the agent

      const agent = new PlanAndCodeAgent({
        apiKey,
        env,
        defaultBranch: repository.data.default_branch,
        issueNumber,
        repository: repository.data,
        sessionToken,
        jobId: workflowId,
      })
      • sessionToken is not further touched inside this file; it becomes input to PlanAndCodeAgent.

Identify downstream consumers of sessionToken (via PlanAndCodeAgent)

In lib/agents/PlanAndCodeAgent.ts:

export interface PlanAndCodeAgentParams extends AgentConstructorParams {
  env: RepoEnvironment
  defaultBranch: string
  repository?: GitHubRepository
  issueNumber?: number
  sessionToken?: string
  jobId?: string
}

export class PlanAndCodeAgent extends ResponsesAPIAgent {
  constructor(params: PlanAndCodeAgentParams) {
    const { env, defaultBranch, repository, issueNumber, sessionToken, jobId, ...base } = params
    // ...
    if (sessionToken && repository) {
      const repoFullName = repoFullNameSchema.parse(repository.full_name)
      this.addTool(createSyncBranchTool(repoFullName, env, sessionToken))
      if (typeof issueNumber === "number") {
        this.addTool(createCreatePRTool(repository, issueNumber))
      }
    }
  }
}

Downstream:

  • createSyncBranchTool(repoFullName, env, sessionToken) (lib/tools/SyncBranchTool.ts)

    async function fnHandler(
      repoFullName: RepoFullName,
      env: RepoEnvironment,
      params: SyncBranchParams,
      token: string
    ) { /* ... */ }
    • Data used from sessionToken: just the raw string, passed as token.
    • Where that token goes:
      • If env.kind === "host": pushBranch(branch, env.root, token, repoFullName.fullName)lib/git.ts.
      • If env.kind === "container":
        • getCloneUrlWithAccessToken(repoFullName.fullName, token) → builds authenticated HTTPS remote URL.
        • execInContainerWithDockerode({ name: env.name, command: 'git remote set-url origin "<url>"' })
        • execInContainerWithDockerode({ name: env.name, command: 'git push origin <branch>' })
  • pushBranch and clone URL composition

    • pushBranch (lib/git.ts):

      if (token && repoFullName) {
        const authenticatedUrl = getCloneUrlWithAccessToken(repoFullName, token)
        await execPromise(`git remote set-url origin "${authenticatedUrl}"`, { cwd })
      }
      await execPromise(`git push origin ${branchName}`, { cwd })
    • getCloneUrlWithAccessToken (lib/utils/utils-common.ts):

      export function getCloneUrlWithAccessToken(userRepo: string, token: string): string {
        if (token.startsWith("ghs_")) {
          return `https://x-access-token:${token}@github.com/${userRepo}.git`
        }
        return `https://${token}@github.com/${userRepo}.git`
      }
    • Semantics: the token is solely used to build an authenticated https://[email protected]/owner/repo.git remote for git push. No other token fields are inspected besides the ghs_ prefix.

  • createCreatePRTool(repository, issueNumber) (lib/tools/CreatePRTool.ts)

    • Note: sessionToken is NOT used here; remote auth for PR creation comes from getGraphQLClient() / getOctokit() elsewhere, not from this specific token.
    • Data needed from repository:
      • repository.full_name → used as repoFullName (split into owner/repo for GitHub API).
      • repository.default_branch → used as baseRefName for the PR.
    const existingPR = await getPullRequestOnBranch({
      repoFullName: repository.full_name,
      branch,
    })
    
    const pr = await createPullRequest({
      repoFullName: repository.full_name,
      branch,
      title,
      body,
      issueNumber,
      baseRefName: repository.default_branch,
    })
    
    await addLabelsToPullRequest({
      repoFullName: repository.full_name,
      pullNumber: pr.data.number,
      labels: ["AI generated"],
    })

Summarize the minimal data actually needed

Putting it together:

  • sessionToken

    • Used for:
      • Auth for a single Octokit call to repos.get inside the workflow.
      • Auth for Git push in the agent’s sync_branch_to_remote tool:
        • Embedding into authenticated clone/remote URLs.
        • Enabling git push origin <branch> for the container/host.
    • Actual required shape:
      • A string compatible with GitHub HTTPS auth, with sufficient scope to:
        • Read repo metadata (repos.get) for owner/repo.
        • Push to owner/repo over HTTPS.
      • No other structure is required; the only additional behavior is the special-casing of ghs_ tokens when building the URL.
  • repository.data (as passed into PlanAndCodeAgent)

    • Fields used in this workflow before passing to the agent:
      • repository.data.default_branch (to choose workingBranch fallback and to initialize the local checkout).
    • Fields used by the agent/tooling:
      • repository.full_name → validated/parsed into { owner, repo, fullName } for:
        • createSyncBranchTool → remote branch existence/creation and push.
        • createCreatePRToolrepoFullName for PR and labels API calls.
      • repository.default_branchbaseRefName for PR creation.
    • Everything else on GitHubRepository is currently unused in this path.
  • Data you already have outside repository.data:

    • repoFullName is an input param to the workflow.
    • issueNumber is an input param.
    • So from a business-logic perspective, you could avoid passing the entire GitHubRepository object to the agent and instead pass:
      • repoFullName: string (existing param)
      • defaultBranch: string
      • issueNumber?: number

Implications for refactoring into ports/adapters

Given the above, you can tighten the workflow’s contract substantially:

  • Business-level workflow port inputs

    • Inputs:

      • issueNumber: number
      • repoFullName: string
      • login: string
      • Optional: jobId, branch
    • Ports to inject (backed by adapters that know about tokens, Octokit, etc.):

    • RepositoryMetadataPort (reads repo info)

      • Methods like:
        • getDefaultBranch(repoFullName): Promise<string>
      • Implementation can hide sessionToken and Octokit; the workflow just receives defaultBranch.
    • RemoteGitPort / BranchSyncPort

      • Capability:
        • ensureBranchPushed({ repoFullName, env, branch }): Promise<void | Result>
      • Implementation internally uses sessionToken + getCloneUrlWithAccessToken (what createSyncBranchTool and pushBranch are doing now).
    • PullRequestPort

      • Capability:
        • createPR({ repoFullName, baseBranch, headBranch, title, body, issueNumber }): Promise<PR> and optional addLabelsToPR.
      • Implementation uses getGraphQLClient / getOctokit and can be independent of sessionToken.
    • PermissionsPort

      • Already effectively exists via checkRepoPermissions(repoFullName); keep that as a port so the workflow doesn’t know about auth.
  • Agent inputs

    • You can reduce PlanAndCodeAgent’s required inputs to:
      • env: RepoEnvironment
      • defaultBranch: string
      • repoFullName: string (instead of full GitHubRepository, if you adjust the constructor to accept that)
      • issueNumber?: number
      • Optionally, an abstracted “branch sync tool factory” that is implemented in terms of sessionToken instead of exposing the token itself.

In short: sessionToken is only needed to (a) fetch repo metadata once and (b) authenticate Git pushes; and the only repository.data fields actually used are full_name and default_branch. Everything else can be hidden behind ports so that the workflow only sees repoFullName, defaultBranch, and high-level operations like “sync branch” and “create PR”.


The above is another agent's assessment of the current usage of our sessionToken variable within the autoResolveIssue workflow.

I would like to refactor this out, and try to keep autoResolveIssue to be focused on business logic. So, its downstream consumers like the octokit that provides the repository data, should be refactored out, so that we use our existing ports or create new ports if needed to provide the necessary repository data to make autoResolveIssue still functional.

These Github adapters also need an authentication provider, since different contexts will provide different authentication methods (in nextjs, we often give the user session token, whereas in the worker, we often use the app installation token).

so the workflow should require the getDefaultBranch port, and any consumers of this workflow should inject the adapter, along with the authentication method.

The agent above also suggested a bunch of other ports that I think are reasonable and we should also implement, such as the RemoteGitPort and BranchSyncPort, etc.

we should try to streamline and tighten the data needed, as suggested by the above agent in the workflow.

So we also don't need to get the repo full name from the GitHub adapter, since that's already provided in the parameters too.

owner,
repo,
})
cloneUrl = getCloneUrlWithAccessToken(repoFullName, installationToken)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this workflow of setupLocalRepository could be done with different types of tokens (user session token, installation token), we should abstract this getCloneUrlWithAccessToken a bit. The subscriber to this function should determine which token to use via injection. So this function can just focus on getting the cloneUrl, without worrying about the actual implementation.

See if you can help separate the concerns here, and make sure you update any subscribers to this function to make sure they follow the new approach.

}
// Use installation-scoped Octokit for this repository (no global token)
const installation = await getInstallationFromRepo({ owner, repo })
const installationOctokit = await getInstallationOctokit(installation.data.id)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authentication here can also be injected, depending on the consumer's context. They may use user session tokens or installation tokens. Elsewhere in this PR we talked about a GitHub auth provider, let's see if we can reuse that here.

@vercel vercel bot temporarily deployed to Preview – issue-to-pr-realtime December 16, 2025 02:11 Inactive
@vercel vercel bot temporarily deployed to Preview – issue-to-pr-storybook December 16, 2025 02:11 Inactive
claude and others added 2 commits January 13, 2026 07:45
Merged latest changes from main while preserving the feature branch's
approach of removing global token state in favor of installation-scoped
GitHub App authentication.

Key conflict resolutions:
- Removed all usage of setAccessToken/getAccessToken global functions
- Kept installation-scoped authentication via getInstallationTokenFromRepo
- Updated import paths from 'shared/' to '@/shared/' per main's changes
- Preserved functionality while eliminating concurrency-unsafe global state

This maintains the architectural goal of using proper installation-scoped
tokens per repository rather than global token state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI generated AI-generated Pull Requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove global setToken usage and refactor token handling

3 participants