Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Manually pass auth token for ado server deployments. [#543](https://github.com/sourcebot-dev/sourcebot/pull/543)

## [4.7.2] - 2025-09-22

### Fixed
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/connections/ado-cloud.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
"repos": [
"organizationName/projectName/repoName",
"organizationName/projectName/repoName2
Expand All @@ -26,6 +27,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
"orgs": [
"organizationName",
"organizationName2
Expand All @@ -37,6 +39,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
"projects": [
"organizationName/projectName",
"organizationName/projectName2"
Expand All @@ -48,6 +51,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
// Include all repos in my-org...
"orgs": [
"my-org"
Expand Down Expand Up @@ -91,6 +95,7 @@ Next, provide the access token via the `token` property, either as an environmen
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
"token": {
// note: this env var can be named anything. It
// doesn't need to be `ADO_TOKEN`.
Expand Down Expand Up @@ -121,6 +126,7 @@ Next, provide the access token via the `token` property, either as an environmen
```json
{
"type": "azuredevops",
"deploymentType": "cloud",
"token": {
"secret": "mysecret"
}
Expand Down
9 changes: 8 additions & 1 deletion docs/docs/connections/ado-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"useTfsPath": true
"deploymentType": "server",
"useTfsPath": true,
"repos": [
"organizationName/projectName/repoName",
"organizationName/projectName/repoName2
Expand All @@ -28,6 +29,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "server",
"repos": [
"organizationName/projectName/repoName",
"organizationName/projectName/repoName2
Expand All @@ -39,6 +41,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "server",
"orgs": [
"collectionName",
"collectionName2"
Expand All @@ -50,6 +53,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "server",
"projects": [
"collectionName/projectName",
"collectionName/projectName2"
Expand All @@ -61,6 +65,7 @@ If you're not familiar with Sourcebot [connections](/docs/connections/overview),
```json
{
"type": "azuredevops",
"deploymentType": "server",
// Include all repos in my-org...
"orgs": [
"my-org"
Expand Down Expand Up @@ -104,6 +109,7 @@ Next, provide the access token via the `token` property, either as an environmen
```json
{
"type": "azuredevops",
"deploymentType": "server",
"token": {
// note: this env var can be named anything. It
// doesn't need to be `ADO_TOKEN`.
Expand Down Expand Up @@ -134,6 +140,7 @@ Next, provide the access token via the `token` property, either as an environmen
```json
{
"type": "azuredevops",
"deploymentType": "server",
"token": {
"secret": "mysecret"
}
Expand Down
4 changes: 2 additions & 2 deletions docs/snippets/schemas/v3/azuredevops.schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"cloud",
"server"
],
"default": "cloud",
"description": "The type of Azure DevOps deployment"
},
"useTfsPath": {
Expand Down Expand Up @@ -199,7 +198,8 @@
},
"required": [
"type",
"token"
"token",
"deploymentType"
],
"additionalProperties": false
}
Expand Down
4 changes: 2 additions & 2 deletions docs/snippets/schemas/v3/connection.schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,6 @@
"cloud",
"server"
],
"default": "cloud",
"description": "The type of Azure DevOps deployment"
},
"useTfsPath": {
Expand Down Expand Up @@ -1068,7 +1067,8 @@
},
"required": [
"type",
"token"
"token",
"deploymentType"
],
"additionalProperties": false
},
Expand Down
4 changes: 2 additions & 2 deletions docs/snippets/schemas/v3/index.schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,6 @@
"cloud",
"server"
],
"default": "cloud",
"description": "The type of Azure DevOps deployment"
},
"useTfsPath": {
Expand Down Expand Up @@ -1351,7 +1350,8 @@
},
"required": [
"type",
"token"
"token",
"deploymentType"
],
"additionalProperties": false
},
Expand Down
31 changes: 24 additions & 7 deletions packages/backend/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ type onProgressFn = (event: SimpleGitProgressEvent) => void;
export const cloneRepository = async (
{
cloneUrl,
authHeader,
path,
onProgress,
}: {
cloneUrl: string,
authHeader?: string,
path: string,
onProgress?: onProgressFn
}
Expand All @@ -24,13 +26,12 @@ export const cloneRepository = async (
path,
})

await git.clone(
cloneUrl,
path,
[
"--bare",
]
);
const cloneArgs = [
"--bare",
...(authHeader ? ["-c", `http.extraHeader=${authHeader}`] : [])
];

await git.clone(cloneUrl, path, cloneArgs);

await unsetGitConfig(path, ["remote.origin.url"]);
} catch (error: unknown) {
Expand All @@ -50,10 +51,12 @@ export const cloneRepository = async (
export const fetchRepository = async (
{
cloneUrl,
authHeader,
path,
onProgress,
}: {
cloneUrl: string,
authHeader?: string,
path: string,
onProgress?: onProgressFn
}
Expand All @@ -65,6 +68,10 @@ export const fetchRepository = async (
path: path,
})

if (authHeader) {
await git.addConfig("http.extraHeader", authHeader);
}

await git.fetch([
cloneUrl,
"+refs/heads/*:refs/heads/*",
Expand All @@ -81,6 +88,16 @@ export const fetchRepository = async (
} else {
throw new Error(`${baseLog}. Error: ${error}`);
}
} finally {
if (authHeader) {
const git = simpleGit({
progress: onProgress,
}).cwd({
path: path,
})

await git.raw(["config", "--unset", "http.extraHeader", authHeader]);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/repoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export class RepoManager {

const credentials = await getAuthCredentialsForRepo(repo, this.db);
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
const authHeader = credentials?.authHeader ?? undefined;

if (existsSync(repoPath) && !isReadOnly) {
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
Expand All @@ -188,6 +189,7 @@ export class RepoManager {
logger.info(`Fetching ${repo.displayName}...`);
const { durationMs } = await measure(() => fetchRepository({
cloneUrl: cloneUrlMaybeWithToken,
authHeader,
path: repoPath,
onProgress: ({ method, stage, progress }) => {
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
Expand All @@ -203,6 +205,7 @@ export class RepoManager {

const { durationMs } = await measure(() => cloneRepository({
cloneUrl: cloneUrlMaybeWithToken,
authHeader,
path: repoPath,
onProgress: ({ method, stage, progress }) => {
logger.debug(`git.${method} ${stage} stage ${progress}% complete for ${repo.displayName}`)
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ export type RepoWithConnections = Repo & { connections: (RepoToConnection & { co
export type RepoAuthCredentials = {
hostUrl?: string;
token: string;
cloneUrlWithToken: string;
cloneUrlWithToken?: string;
authHeader?: string;
}
40 changes: 26 additions & 14 deletions packages/backend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,31 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
if (config.token) {
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
return {
hostUrl: config.url,
token,
cloneUrlWithToken: createGitCloneUrlWithToken(
repo.cloneUrl,
{
// @note: If we don't provide a username, the password will be set as the username. This seems to work
// for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password
// is set correctly
username: 'user',
password: token
}
),

// For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default
// to this over basic auth. As a result, we cannot embed the token in the clone URL and must force basic auth by passing in the token
// appropriately in the header. To do this, we set the authHeader field here
if (config.deploymentType === 'server') {
return {
hostUrl: config.url,
token,
authHeader: "Authorization: Basic " + Buffer.from(`:${token}`).toString('base64')
}
} else {
return {
hostUrl: config.url,
token,
cloneUrlWithToken: createGitCloneUrlWithToken(
repo.cloneUrl,
{
// @note: If we don't provide a username, the password will be set as the username. This seems to work
// for ADO cloud but not for ADO server. To fix this, we set a placeholder username to ensure the password
// is set correctly
username: 'user',
password: token
}
),
}
}
}
}
Expand All @@ -228,4 +240,4 @@ const createGitCloneUrlWithToken = (cloneUrl: string, credentials: { username?:
url.password = credentials.password;
}
return url.toString();
}
}
4 changes: 2 additions & 2 deletions packages/schemas/src/v3/azuredevops.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ const schema = {
"cloud",
"server"
],
"default": "cloud",
"description": "The type of Azure DevOps deployment"
},
"useTfsPath": {
Expand Down Expand Up @@ -198,7 +197,8 @@ const schema = {
},
"required": [
"type",
"token"
"token",
"deploymentType"
],
"additionalProperties": false
} as const;
Expand Down
2 changes: 1 addition & 1 deletion packages/schemas/src/v3/azuredevops.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface AzureDevOpsConnectionConfig {
/**
* The type of Azure DevOps deployment
*/
deploymentType?: "cloud" | "server";
deploymentType: "cloud" | "server";
/**
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/schemas/src/v3/connection.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,6 @@ const schema = {
"cloud",
"server"
],
"default": "cloud",
"description": "The type of Azure DevOps deployment"
},
"useTfsPath": {
Expand Down Expand Up @@ -1067,7 +1066,8 @@ const schema = {
},
"required": [
"type",
"token"
"token",
"deploymentType"
],
"additionalProperties": false
},
Expand Down
2 changes: 1 addition & 1 deletion packages/schemas/src/v3/connection.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export interface AzureDevOpsConnectionConfig {
/**
* The type of Azure DevOps deployment
*/
deploymentType?: "cloud" | "server";
deploymentType: "cloud" | "server";
/**
* Use legacy TFS path format (/tfs) in API URLs. Required for older TFS installations (TFS 2018 and earlier). When true, API URLs will include /tfs in the path (e.g., https://server/tfs/collection/_apis/...).
*/
Expand Down
Loading