From 9efa34597095f9d9122bd8737ff60233956f9175 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 13 Jul 2026 16:52:17 -0700 Subject: [PATCH] fix(worker): use Ask GitHub PAT for repository auth --- packages/backend/src/utils.test.ts | 32 ++++++++++++++++++++++++++++-- packages/backend/src/utils.ts | 21 ++++++++++++++++++-- packages/shared/src/env.server.ts | 1 + 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/utils.test.ts b/packages/backend/src/utils.test.ts index 524c354f4..b8d2e9753 100644 --- a/packages/backend/src/utils.test.ts +++ b/packages/backend/src/utils.test.ts @@ -1,8 +1,9 @@ import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest'; -import { arraysEqualShallow, fetchWithRetry } from './utils'; -import { isRemotePath } from '@sourcebot/shared'; +import { arraysEqualShallow, fetchWithRetry, getAuthCredentialsForRepo } from './utils'; +import { env, isRemotePath } from '@sourcebot/shared'; import { Logger } from 'winston'; import { RequestError } from '@octokit/request-error'; +import type { RepoWithConnections } from './types'; vi.mock('@sentry/node', () => ({ captureException: vi.fn(), @@ -15,6 +16,33 @@ const createMockLogger = (): Logger => ({ debug: vi.fn(), } as unknown as Logger); +describe('getAuthCredentialsForRepo', () => { + const originalAskGithubToken = env.EXPERIMENT_ASK_GH_GITHUB_TOKEN; + + afterEach(() => { + env.EXPERIMENT_ASK_GH_GITHUB_TOKEN = originalAskGithubToken; + }); + + test('uses the Ask GitHub PAT before other GitHub credentials', async () => { + env.EXPERIMENT_ASK_GH_GITHUB_TOKEN = 'github-pat-token'; + const repo = { + displayName: 'codemirror/dev', + external_codeHostType: 'github', + external_codeHostUrl: 'https://github.com', + cloneUrl: 'https://github.com/codemirror/dev.git', + connections: [], + } as RepoWithConnections; + + const credentials = await getAuthCredentialsForRepo(repo); + + expect(credentials).toEqual({ + hostUrl: 'https://github.com', + token: 'github-pat-token', + cloneUrlWithToken: 'https://x-access-token:github-pat-token@github.com/codemirror/dev.git', + }); + }); +}); + test('should return true for identical arrays', () => { expect(arraysEqualShallow([1, 2, 3], [1, 2, 3])).toBe(true); }); diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index d99727ea3..a0c3cf028 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -1,7 +1,7 @@ import { Logger } from "winston"; import { RepoAuthCredentials, RepoWithConnections } from "./types.js"; import path from 'path'; -import { getTokenFromConfig } from "@sourcebot/shared"; +import { env, getTokenFromConfig } from "@sourcebot/shared"; import * as Sentry from "@sentry/node"; import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type'; import { GithubAppManager } from "./ee/githubAppManager.js"; @@ -115,6 +115,23 @@ export const fetchWithRetry = async ( // may have their own token. This method will just pick the first connection that has a token (if one exists) and uses that. This // may technically cause syncing to fail if that connection's token just so happens to not have access to the repo it's referencing. export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logger?: Logger): Promise => { + if (repo.external_codeHostType === 'github' && env.EXPERIMENT_ASK_GH_GITHUB_TOKEN) { + logger?.debug(`Using Ask GitHub PAT for service auth for repo ${repo.displayName} hosted at ${repo.external_codeHostUrl}`); + + const token = env.EXPERIMENT_ASK_GH_GITHUB_TOKEN; + return { + hostUrl: repo.external_codeHostUrl, + token, + cloneUrlWithToken: createGitCloneUrlWithToken( + repo.cloneUrl, + { + username: 'x-access-token', + password: token, + } + ), + }; + } + // If we have github apps configured we assume that we must use them for github service auth if (repo.external_codeHostType === 'github' && await hasEntitlement('github-app') && GithubAppManager.getInstance().appsConfigured()) { logger?.debug(`Using GitHub App for service auth for repo ${repo.displayName} hosted at ${repo.external_codeHostUrl}`); @@ -284,4 +301,4 @@ export const setIntervalAsync = (target: () => Promise, pollingIntervalMs: setIntervalWithPromise(target), pollingIntervalMs ); -} \ No newline at end of file +} diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 1f96a188d..392a18517 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -363,6 +363,7 @@ const options = { EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN: z.string().optional(), PERMISSION_SYNC_REPO_DRIVEN_ENABLED: booleanSchema.default('true'), EXPERIMENT_ASK_GH_ENABLED: booleanSchema.default('false'), + EXPERIMENT_ASK_GH_GITHUB_TOKEN: z.string().optional(), // Used as the key for AES-256-CBC encryption (@see shared/src/crypto.ts). // The key is read as ASCII (1 char = 1 byte), so AES-256's 32-byte key