mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
Add Bitbucket support (#275)
* [wip] add bitbucket schema * wip bitbucket support * add support for pulling bitbucket repos and UI support for bitbucket * fix bitbucket app password auth case * add back support for multiple workspaces and add exclude logic * add branches to bitbucket * add bitbucket server support * add docs for bitbucket and minor nits * doc nits * code rabbit fixes * fix build error * add bitbucket web ui support * misc cleanups and fix ui issues with bitbucket connections * add changelog entry
This commit is contained in:
parent
2acb1e558f
commit
b6dedc78ba
41 changed files with 2415 additions and 61 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
- [Sourcebot EE] Added search contexts, user-defined groupings of repositories that help focus searches on specific areas of a codebase. [#273](https://github.com/sourcebot-dev/sourcebot/pull/273)
|
||||
- Added support for Bitbucket Cloud and Bitbucket Data Center connections. [#275](https://github.com/sourcebot-dev/sourcebot/pull/275)
|
||||
|
||||
|
||||
## [3.0.4] - 2025-04-12
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@
|
|||
"docs/connections/overview",
|
||||
"docs/connections/github",
|
||||
"docs/connections/gitlab",
|
||||
"docs/connections/bitbucket-cloud",
|
||||
"docs/connections/bitbucket-data-center",
|
||||
"docs/connections/gitea",
|
||||
"docs/connections/gerrit",
|
||||
"docs/connections/request-new"
|
||||
|
|
|
|||
201
docs/docs/connections/bitbucket-cloud.mdx
Normal file
201
docs/docs/connections/bitbucket-cloud.mdx
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
---
|
||||
title: Linking code from Bitbucket Cloud
|
||||
sidebarTitle: Bitbucket Cloud
|
||||
---
|
||||
|
||||
import BitbucketToken from '/snippets/bitbucket-token.mdx';
|
||||
import BitbucketAppPassword from '/snippets/bitbucket-app-password.mdx';
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Sync individual repos">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
"repos": [
|
||||
"myWorkspace/myRepo"
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Sync all repos in a workspace">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
"workspaces": [
|
||||
"myWorkspace"
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Sync all repos in a project">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
"projects": [
|
||||
"myProject"
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Exclude repos from syncing">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
// Include all repos in my-workspace...
|
||||
"workspaces": [
|
||||
"myWorkspace"
|
||||
],
|
||||
// ...except:
|
||||
"exclude": {
|
||||
// repos that are archived
|
||||
"archived": true,
|
||||
// repos that are forks
|
||||
"forks": true,
|
||||
// repos that match these glob patterns
|
||||
"repos": [
|
||||
"myWorkspace/repo1",
|
||||
"myWorkspace2/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Authenticating with Bitbucket Cloud
|
||||
|
||||
In order to index private repositories, you'll need to provide authentication credentials. You can do this using an `App Password` or an `Access Token`
|
||||
|
||||
<Tabs>
|
||||
<Tab title="App Password">
|
||||
Navigate to the [app password creation page](https://bitbucket.org/account/settings/app-passwords/) and create an app password. Ensure that it has the proper permissions for the scope
|
||||
of info you want to fetch (i.e. workspace, project, and/or repo level)
|
||||

|
||||
|
||||
Next, provide your username + app password pair to Sourcebot:
|
||||
|
||||
<BitbucketAppPassword />
|
||||
</Tab>
|
||||
<Tab title="Access Token">
|
||||
Create an access token for the desired scope (repo, project, or workspace). Visit the official [Bitbucket Cloud docs](https://support.atlassian.com/bitbucket-cloud/docs/access-tokens/)
|
||||
for more info.
|
||||
|
||||
Next, provide the access token to Sourcebot:
|
||||
|
||||
<BitbucketToken />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Schema reference
|
||||
|
||||
<Accordion title="Reference">
|
||||
[schemas/v3/bitbucket.json](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/bitbucket.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"$ref": "./shared.json#/definitions/Token",
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": ["cloud", "server"],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"$ref": "./shared.json#/definitions/GitRevisions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
180
docs/docs/connections/bitbucket-data-center.mdx
Normal file
180
docs/docs/connections/bitbucket-data-center.mdx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
title: Linking code from Bitbucket Data Center
|
||||
sidebarTitle: Bitbucket Data Center
|
||||
---
|
||||
|
||||
import BitbucketToken from '/snippets/bitbucket-token.mdx';
|
||||
import BitbucketAppPassword from '/snippets/bitbucket-app-password.mdx';
|
||||
|
||||
## Examples
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Sync individual repos">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "server",
|
||||
"url": "https://mybitbucketdeployment.com",
|
||||
"repos": [
|
||||
"myProject/myRepo"
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Sync all repos in a project">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "server",
|
||||
"url": "https://mybitbucketdeployment.com",
|
||||
"projects": [
|
||||
"myProject"
|
||||
]
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Exclude repos from syncing">
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "server",
|
||||
"url": "https://mybitbucketdeployment.com",
|
||||
// Include all repos in myProject...
|
||||
"projects": [
|
||||
"myProject"
|
||||
],
|
||||
// ...except:
|
||||
"exclude": {
|
||||
// repos that are archived
|
||||
"archived": true,
|
||||
// repos that are forks
|
||||
"forks": true,
|
||||
// repos that match these glob patterns
|
||||
"repos": [
|
||||
"myProject/repo1",
|
||||
"myProject2/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Authenticating with Bitbucket Data Center
|
||||
|
||||
In order to index private repositories, you'll need to provide an access token to Sourcebot.
|
||||
|
||||
Create an access token for the desired scope (repo, project, or workspace). Visit the official [Bitbucket Data Center docs](https://confluence.atlassian.com/bitbucketserver/http-access-tokens-939515499.html)
|
||||
for more info.
|
||||
|
||||
Next, provide the access token to Sourcebot:
|
||||
|
||||
<BitbucketToken />
|
||||
|
||||
|
||||
## Schema reference
|
||||
|
||||
<Accordion title="Reference">
|
||||
[schemas/v3/bitbucket.json](https://github.com/sourcebot-dev/sourcebot/blob/main/schemas/v3/bitbucket.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"$ref": "./shared.json#/definitions/Token",
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": ["cloud", "server"],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"$ref": "./shared.json#/definitions/GitRevisions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
|
@ -26,6 +26,8 @@ There are two ways to define connections:
|
|||
<CardGroup cols={2}>
|
||||
<Card horizontal title="GitHub" icon="github" href="/docs/connections/github" />
|
||||
<Card horizontal title="GitLab" icon="gitlab" href="/docs/connections/gitlab" />
|
||||
<Card horizontal title="Bitbucket Cloud" icon="bitbucket" href="/docs/connections/bitbucket-cloud" />
|
||||
<Card horizontal title="Bitbucket Data Center" icon="bitbucket" href="/docs/connections/bitbucket-data-center" />
|
||||
<Card horizontal title="Gitea" href="/docs/connections/gitea" />
|
||||
<Card horizontal title="Gerrit" href="/docs/connections/gerrit" />
|
||||
</CardGroup>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
title: "Overview"
|
||||
---
|
||||
|
||||
import ConnectionCards from '/snippets/connection-cards.mdx';
|
||||
|
||||
Sourcebot is an **[open-source](https://github.com/sourcebot-dev/sourcebot) code search tool** that is purpose built to search multi-million line codebases in seconds. It integrates with [GitHub](/docs/connections/github), [GitLab](/docs/connections/gitlab), and [other platforms](/docs/connections).
|
||||
|
||||
## Getting Started
|
||||
|
|
|
|||
BIN
docs/images/bitbucket_app_password_perms.png
Normal file
BIN
docs/images/bitbucket_app_password_perms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
|
|
@ -76,8 +76,10 @@ Sourcebot is open source and can be self-hosted using our official [Docker image
|
|||
Sourcebot supports indexing public & private code on the following code hosts:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card horizontal title="GitHub" href="/docs/connections/github" />
|
||||
<Card horizontal title="GitLab" href="/docs/connections/gitlab" />
|
||||
<Card horizontal title="GitHub" icon="github" href="/docs/connections/github" />
|
||||
<Card horizontal title="GitLab" icon="gitlab" href="/docs/connections/gitlab" />
|
||||
<Card horizontal title="Bitbucket Cloud" icon="bitbucket" href="/docs/connections/bitbucket-cloud" />
|
||||
<Card horizontal title="Bitbucket Data Center" icon="bitbucket" href="/docs/connections/bitbucket-data-center" />
|
||||
<Card horizontal title="Gitea" href="/docs/connections/gitea" />
|
||||
<Card horizontal title="Gerrit" href="/docs/connections/gerrit" />
|
||||
</CardGroup>
|
||||
|
|
|
|||
51
docs/snippets/bitbucket-app-password.mdx
Normal file
51
docs/snippets/bitbucket-app-password.mdx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<Tabs>
|
||||
<Tab title="Environment Variable">
|
||||
<Note>Environment variables are only supported in a [declarative config](/self-hosting/more/declarative-config) and cannot be used in the web UI.</Note>
|
||||
|
||||
1. Add the `token` and `user` (username associated with the app password you created) properties to your connection config:
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
"user": "myusername",
|
||||
"token": {
|
||||
// note: this env var can be named anything. It
|
||||
// doesn't need to be `BITBUCKET_TOKEN`.
|
||||
"env": "BITBUCKET_TOKEN"
|
||||
}
|
||||
// .. rest of config ..
|
||||
}
|
||||
```
|
||||
|
||||
2. Pass this environment variable each time you run Sourcebot:
|
||||
```bash
|
||||
docker run \
|
||||
-e BITBUCKET_TOKEN=<PAT> \
|
||||
/* additional args */ \
|
||||
ghcr.io/sourcebot-dev/sourcebot:latest
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Secret">
|
||||
<Note>Secrets are only supported when [authentication](/self-hosting/more/authentication) is enabled.</Note>
|
||||
|
||||
1. Navigate to **Secrets** in settings and create a new secret with your access token:
|
||||
|
||||

|
||||
|
||||
2. Add the `token` and `user` (username associated with the app password you created) properties to your connection config:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"deploymentType": "cloud",
|
||||
"user": "myusername",
|
||||
"token": {
|
||||
"secret": "mysecret"
|
||||
}
|
||||
// .. rest of config ..
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
47
docs/snippets/bitbucket-token.mdx
Normal file
47
docs/snippets/bitbucket-token.mdx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<Tabs>
|
||||
<Tab title="Environment Variable">
|
||||
<Note>Environment variables are only supported in a [declarative config](/self-hosting/more/declarative-config) and cannot be used in the web UI.</Note>
|
||||
|
||||
1. Add the `token` property to your connection config:
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"token": {
|
||||
// note: this env var can be named anything. It
|
||||
// doesn't need to be `BITBUCKET_TOKEN`.
|
||||
"env": "BITBUCKET_TOKEN"
|
||||
}
|
||||
// .. rest of config ..
|
||||
}
|
||||
```
|
||||
|
||||
2. Pass this environment variable each time you run Sourcebot:
|
||||
```bash
|
||||
docker run \
|
||||
-e BITBUCKET_TOKEN=<PAT> \
|
||||
/* additional args */ \
|
||||
ghcr.io/sourcebot-dev/sourcebot:latest
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Secret">
|
||||
<Note>Secrets are only supported when [authentication](/self-hosting/more/authentication) is enabled.</Note>
|
||||
|
||||
1. Navigate to **Secrets** in settings and create a new secret with your PAT:
|
||||
|
||||

|
||||
|
||||
2. Add the `token` property to your connection config:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "bitbucket",
|
||||
"token": {
|
||||
"secret": "mysecret"
|
||||
}
|
||||
// .. rest of config ..
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<CardGroup cols={2}>
|
||||
<Card title="GitHub" icon="github" href="/docs/connections/github"></Card>
|
||||
<Card title="GitLab" icon="gitlab" href="/docs/connections/gitlab"></Card>
|
||||
</CardGroup>
|
||||
|
|
@ -20,5 +20,8 @@
|
|||
"dotenv-cli": "^8.0.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
},
|
||||
"packageManager": "yarn@4.7.0"
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"dependencies": {
|
||||
"@coderabbitai/bitbucket": "^1.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
553
packages/backend/src/bitbucket.ts
Normal file
553
packages/backend/src/bitbucket.ts
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
import { createBitbucketCloudClient } from "@coderabbitai/bitbucket/cloud";
|
||||
import { createBitbucketServerClient } from "@coderabbitai/bitbucket/server";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
|
||||
import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
SchemaRepository as CloudRepository,
|
||||
} from "@coderabbitai/bitbucket/cloud/openapi";
|
||||
import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi";
|
||||
import { processPromiseResults } from "./connectionUtils.js";
|
||||
import { throwIfAnyFailed } from "./connectionUtils.js";
|
||||
|
||||
const logger = createLogger("Bitbucket");
|
||||
const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org';
|
||||
const BITBUCKET_CLOUD_API = 'https://api.bitbucket.org/2.0';
|
||||
const BITBUCKET_CLOUD = "cloud";
|
||||
const BITBUCKET_SERVER = "server";
|
||||
|
||||
export type BitbucketRepository = CloudRepository | ServerRepository;
|
||||
|
||||
interface BitbucketClient {
|
||||
deploymentType: string;
|
||||
token: string | undefined;
|
||||
apiClient: any;
|
||||
baseUrl: string;
|
||||
gitUrl: string;
|
||||
getReposForWorkspace: (client: BitbucketClient, workspaces: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundWorkspaces: string[]}>;
|
||||
getReposForProjects: (client: BitbucketClient, projects: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundProjects: string[]}>;
|
||||
getRepos: (client: BitbucketClient, repos: string[]) => Promise<{validRepos: BitbucketRepository[], notFoundRepos: string[]}>;
|
||||
shouldExcludeRepo: (repo: BitbucketRepository, config: BitbucketConnectionConfig) => boolean;
|
||||
}
|
||||
|
||||
type CloudAPI = ReturnType<typeof createBitbucketCloudClient>;
|
||||
type CloudGetRequestPath = ClientPathsWithMethod<CloudAPI, "get">;
|
||||
|
||||
type ServerAPI = ReturnType<typeof createBitbucketServerClient>;
|
||||
type ServerGetRequestPath = ClientPathsWithMethod<ServerAPI, "get">;
|
||||
|
||||
type CloudPaginatedResponse<T> = {
|
||||
readonly next?: string;
|
||||
readonly page?: number;
|
||||
readonly pagelen?: number;
|
||||
readonly previous?: string;
|
||||
readonly size?: number;
|
||||
readonly values?: readonly T[];
|
||||
}
|
||||
|
||||
type ServerPaginatedResponse<T> = {
|
||||
readonly size: number;
|
||||
readonly limit: number;
|
||||
readonly isLastPage: boolean;
|
||||
readonly values: readonly T[];
|
||||
readonly start: number;
|
||||
readonly nextPageStart: number;
|
||||
}
|
||||
|
||||
export const getBitbucketReposFromConfig = async (config: BitbucketConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
undefined;
|
||||
|
||||
if (config.deploymentType === 'server' && !config.url) {
|
||||
throw new Error('URL is required for Bitbucket Server');
|
||||
}
|
||||
|
||||
const client = config.deploymentType === 'server' ?
|
||||
serverClient(config.url!, config.user, token) :
|
||||
cloudClient(config.user, token);
|
||||
|
||||
let allRepos: BitbucketRepository[] = [];
|
||||
let notFound: {
|
||||
orgs: string[],
|
||||
users: string[],
|
||||
repos: string[],
|
||||
} = {
|
||||
orgs: [],
|
||||
users: [],
|
||||
repos: [],
|
||||
};
|
||||
|
||||
if (config.workspaces) {
|
||||
const { validRepos, notFoundWorkspaces } = await client.getReposForWorkspace(client, config.workspaces);
|
||||
allRepos = allRepos.concat(validRepos);
|
||||
notFound.orgs = notFoundWorkspaces;
|
||||
}
|
||||
|
||||
if (config.projects) {
|
||||
const { validRepos, notFoundProjects } = await client.getReposForProjects(client, config.projects);
|
||||
allRepos = allRepos.concat(validRepos);
|
||||
notFound.orgs = notFoundProjects;
|
||||
}
|
||||
|
||||
if (config.repos) {
|
||||
const { validRepos, notFoundRepos } = await client.getRepos(client, config.repos);
|
||||
allRepos = allRepos.concat(validRepos);
|
||||
notFound.repos = notFoundRepos;
|
||||
}
|
||||
|
||||
const filteredRepos = allRepos.filter((repo) => {
|
||||
return !client.shouldExcludeRepo(repo, config);
|
||||
});
|
||||
|
||||
return {
|
||||
validRepos: filteredRepos,
|
||||
notFound,
|
||||
};
|
||||
}
|
||||
|
||||
function cloudClient(user: string | undefined, token: string | undefined): BitbucketClient {
|
||||
|
||||
const authorizationString =
|
||||
token
|
||||
? !user || user == "x-token-auth"
|
||||
? `Bearer ${token}`
|
||||
: `Basic ${Buffer.from(`${user}:${token}`).toString('base64')}`
|
||||
: undefined;
|
||||
|
||||
const clientOptions: ClientOptions = {
|
||||
baseUrl: BITBUCKET_CLOUD_API,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(authorizationString ? { Authorization: authorizationString } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const apiClient = createBitbucketCloudClient(clientOptions);
|
||||
var client: BitbucketClient = {
|
||||
deploymentType: BITBUCKET_CLOUD,
|
||||
token: token,
|
||||
apiClient: apiClient,
|
||||
baseUrl: BITBUCKET_CLOUD_API,
|
||||
gitUrl: BITBUCKET_CLOUD_GIT,
|
||||
getReposForWorkspace: cloudGetReposForWorkspace,
|
||||
getReposForProjects: cloudGetReposForProjects,
|
||||
getRepos: cloudGetRepos,
|
||||
shouldExcludeRepo: cloudShouldExcludeRepo,
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to do `V extends CloudGetRequestPath` since we will need to call `apiClient.GET(url, ...)`, which
|
||||
* expects `url` to be of type `CloudGetRequestPath`. See example.
|
||||
**/
|
||||
const getPaginatedCloud = async <T>(
|
||||
path: CloudGetRequestPath,
|
||||
get: (url: CloudGetRequestPath) => Promise<CloudPaginatedResponse<T>>
|
||||
): Promise<T[]> => {
|
||||
const results: T[] = [];
|
||||
let url = path;
|
||||
|
||||
while (true) {
|
||||
const response = await get(url);
|
||||
|
||||
if (!response.values || response.values.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(...response.values);
|
||||
|
||||
if (!response.next) {
|
||||
break;
|
||||
}
|
||||
|
||||
url = response.next as CloudGetRequestPath;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
async function cloudGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: CloudRepository[], notFoundWorkspaces: string[]}> {
|
||||
const results = await Promise.allSettled(workspaces.map(async (workspace) => {
|
||||
try {
|
||||
logger.debug(`Fetching all repos for workspace ${workspace}...`);
|
||||
|
||||
const path = `/repositories/${workspace}` as CloudGetRequestPath;
|
||||
const { durationMs, data } = await measure(async () => {
|
||||
const fetchFn = () => getPaginatedCloud<CloudRepository>(path, async (url) => {
|
||||
const response = await client.apiClient.GET(url, {
|
||||
params: {
|
||||
path: {
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
});
|
||||
const { data, error } = response;
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch projects for workspace ${workspace}: ${JSON.stringify(error)}`);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return fetchWithRetry(fetchFn, `workspace ${workspace}`, logger);
|
||||
});
|
||||
logger.debug(`Found ${data.length} repos for workspace ${workspace} in ${durationMs}ms.`);
|
||||
|
||||
return {
|
||||
type: 'valid' as const,
|
||||
data: data,
|
||||
};
|
||||
} catch (e: any) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(`Failed to get repos for workspace ${workspace}: ${e}`);
|
||||
|
||||
const status = e?.cause?.response?.status;
|
||||
if (status == 404) {
|
||||
logger.error(`Workspace ${workspace} not found or invalid access`)
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: workspace
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
throwIfAnyFailed(results);
|
||||
const { validItems: validRepos, notFoundItems: notFoundWorkspaces } = processPromiseResults(results);
|
||||
return {
|
||||
validRepos,
|
||||
notFoundWorkspaces,
|
||||
};
|
||||
}
|
||||
|
||||
async function cloudGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{validRepos: CloudRepository[], notFoundProjects: string[]}> {
|
||||
const results = await Promise.allSettled(projects.map(async (project) => {
|
||||
const [workspace, project_name] = project.split('/');
|
||||
if (!workspace || !project_name) {
|
||||
logger.error(`Invalid project ${project}`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: project
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Fetching all repos for project ${project} for workspace ${workspace}...`);
|
||||
try {
|
||||
const path = `/repositories/${workspace}` as CloudGetRequestPath;
|
||||
const repos = await getPaginatedCloud<CloudRepository>(path, async (url) => {
|
||||
const response = await client.apiClient.GET(url, {
|
||||
params: {
|
||||
query: {
|
||||
q: `project.key="${project_name}"`
|
||||
}
|
||||
}
|
||||
});
|
||||
const { data, error } = response;
|
||||
if (error) {
|
||||
throw new Error (`Failed to fetch projects for workspace ${workspace}: ${error.type}`);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
logger.debug(`Found ${repos.length} repos for project ${project_name} for workspace ${workspace}.`);
|
||||
return {
|
||||
type: 'valid' as const,
|
||||
data: repos
|
||||
}
|
||||
} catch (e: any) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(`Failed to fetch repos for project ${project_name}: ${e}`);
|
||||
|
||||
const status = e?.cause?.response?.status;
|
||||
if (status == 404) {
|
||||
logger.error(`Project ${project_name} not found in ${workspace} or invalid access`)
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: project
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
throwIfAnyFailed(results);
|
||||
const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults(results);
|
||||
return {
|
||||
validRepos,
|
||||
notFoundProjects
|
||||
}
|
||||
}
|
||||
|
||||
async function cloudGetRepos(client: BitbucketClient, repos: string[]): Promise<{validRepos: CloudRepository[], notFoundRepos: string[]}> {
|
||||
const results = await Promise.allSettled(repos.map(async (repo) => {
|
||||
const [workspace, repo_slug] = repo.split('/');
|
||||
if (!workspace || !repo_slug) {
|
||||
logger.error(`Invalid repo ${repo}`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: repo
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug(`Fetching repo ${repo_slug} for workspace ${workspace}...`);
|
||||
try {
|
||||
const path = `/repositories/${workspace}/${repo_slug}` as CloudGetRequestPath;
|
||||
const response = await client.apiClient.GET(path);
|
||||
const { data, error } = response;
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch repo ${repo}: ${error.type}`);
|
||||
}
|
||||
return {
|
||||
type: 'valid' as const,
|
||||
data: [data]
|
||||
};
|
||||
} catch (e: any) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(`Failed to fetch repo ${repo}: ${e}`);
|
||||
|
||||
const status = e?.cause?.response?.status;
|
||||
if (status === 404) {
|
||||
logger.error(`Repo ${repo} not found in ${workspace} or invalid access`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: repo
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
throwIfAnyFailed(results);
|
||||
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results);
|
||||
return {
|
||||
validRepos,
|
||||
notFoundRepos
|
||||
};
|
||||
}
|
||||
|
||||
function cloudShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
|
||||
const cloudRepo = repo as CloudRepository;
|
||||
|
||||
const shouldExclude = (() => {
|
||||
if (config.exclude?.repos && config.exclude.repos.includes(cloudRepo.full_name!)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!config.exclude?.archived) {
|
||||
logger.warn(`Exclude archived repos flag provided in config but Bitbucket Cloud does not support archived repos. Ignoring...`);
|
||||
}
|
||||
|
||||
if (!!config.exclude?.forks && cloudRepo.parent !== undefined) {
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (shouldExclude) {
|
||||
logger.debug(`Excluding repo ${cloudRepo.full_name} because it matches the exclude pattern`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function serverClient(url: string, user: string | undefined, token: string | undefined): BitbucketClient {
|
||||
const authorizationString = (() => {
|
||||
// If we're not given any credentials we return an empty auth string. This will only work if the project/repos are public
|
||||
if(!user && !token) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// A user must be provided when using basic auth
|
||||
// https://developer.atlassian.com/server/bitbucket/rest/v906/intro/#authentication
|
||||
if (!user || user == "x-token-auth") {
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
return `Basic ${Buffer.from(`${user}:${token}`).toString('base64')}`;
|
||||
})();
|
||||
const clientOptions: ClientOptions = {
|
||||
baseUrl: url,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: authorizationString,
|
||||
},
|
||||
};
|
||||
|
||||
const apiClient = createBitbucketServerClient(clientOptions);
|
||||
var client: BitbucketClient = {
|
||||
deploymentType: BITBUCKET_SERVER,
|
||||
token: token,
|
||||
apiClient: apiClient,
|
||||
baseUrl: url,
|
||||
gitUrl: url,
|
||||
getReposForWorkspace: serverGetReposForWorkspace,
|
||||
getReposForProjects: serverGetReposForProjects,
|
||||
getRepos: serverGetRepos,
|
||||
shouldExcludeRepo: serverShouldExcludeRepo,
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
const getPaginatedServer = async <T>(
|
||||
path: ServerGetRequestPath,
|
||||
get: (url: ServerGetRequestPath, start?: number) => Promise<ServerPaginatedResponse<T>>
|
||||
): Promise<T[]> => {
|
||||
const results: T[] = [];
|
||||
let nextStart: number | undefined;
|
||||
|
||||
while (true) {
|
||||
const response = await get(path, nextStart);
|
||||
|
||||
if (!response.values || response.values.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
results.push(...response.values);
|
||||
|
||||
if (response.isLastPage) {
|
||||
break;
|
||||
}
|
||||
|
||||
nextStart = response.nextPageStart;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function serverGetReposForWorkspace(client: BitbucketClient, workspaces: string[]): Promise<{validRepos: ServerRepository[], notFoundWorkspaces: string[]}> {
|
||||
logger.debug('Workspaces are not supported in Bitbucket Server');
|
||||
return {
|
||||
validRepos: [],
|
||||
notFoundWorkspaces: workspaces
|
||||
};
|
||||
}
|
||||
|
||||
async function serverGetReposForProjects(client: BitbucketClient, projects: string[]): Promise<{validRepos: ServerRepository[], notFoundProjects: string[]}> {
|
||||
const results = await Promise.allSettled(projects.map(async (project) => {
|
||||
try {
|
||||
logger.debug(`Fetching all repos for project ${project}...`);
|
||||
|
||||
const path = `/rest/api/1.0/projects/${project}/repos` as ServerGetRequestPath;
|
||||
const { durationMs, data } = await measure(async () => {
|
||||
const fetchFn = () => getPaginatedServer<ServerRepository>(path, async (url, start) => {
|
||||
const response = await client.apiClient.GET(url, {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
}
|
||||
}
|
||||
});
|
||||
const { data, error } = response;
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch repos for project ${project}: ${JSON.stringify(error)}`);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return fetchWithRetry(fetchFn, `project ${project}`, logger);
|
||||
});
|
||||
logger.debug(`Found ${data.length} repos for project ${project} in ${durationMs}ms.`);
|
||||
|
||||
return {
|
||||
type: 'valid' as const,
|
||||
data: data,
|
||||
};
|
||||
} catch (e: any) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(`Failed to get repos for project ${project}: ${e}`);
|
||||
|
||||
const status = e?.cause?.response?.status;
|
||||
if (status == 404) {
|
||||
logger.error(`Project ${project} not found or invalid access`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: project
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
throwIfAnyFailed(results);
|
||||
const { validItems: validRepos, notFoundItems: notFoundProjects } = processPromiseResults(results);
|
||||
return {
|
||||
validRepos,
|
||||
notFoundProjects
|
||||
};
|
||||
}
|
||||
|
||||
async function serverGetRepos(client: BitbucketClient, repos: string[]): Promise<{validRepos: ServerRepository[], notFoundRepos: string[]}> {
|
||||
const results = await Promise.allSettled(repos.map(async (repo) => {
|
||||
const [project, repo_slug] = repo.split('/');
|
||||
if (!project || !repo_slug) {
|
||||
logger.error(`Invalid repo ${repo}`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: repo
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug(`Fetching repo ${repo_slug} for project ${project}...`);
|
||||
try {
|
||||
const path = `/rest/api/1.0/projects/${project}/repos/${repo_slug}` as ServerGetRequestPath;
|
||||
const response = await client.apiClient.GET(path);
|
||||
const { data, error } = response;
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch repo ${repo}: ${error.type}`);
|
||||
}
|
||||
return {
|
||||
type: 'valid' as const,
|
||||
data: [data]
|
||||
};
|
||||
} catch (e: any) {
|
||||
Sentry.captureException(e);
|
||||
logger.error(`Failed to fetch repo ${repo}: ${e}`);
|
||||
|
||||
const status = e?.cause?.response?.status;
|
||||
if (status === 404) {
|
||||
logger.error(`Repo ${repo} not found in project ${project} or invalid access`);
|
||||
return {
|
||||
type: 'notFound' as const,
|
||||
value: repo
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}));
|
||||
|
||||
throwIfAnyFailed(results);
|
||||
const { validItems: validRepos, notFoundItems: notFoundRepos } = processPromiseResults(results);
|
||||
return {
|
||||
validRepos,
|
||||
notFoundRepos
|
||||
};
|
||||
}
|
||||
|
||||
function serverShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
|
||||
const serverRepo = repo as ServerRepository;
|
||||
|
||||
const projectName = serverRepo.project!.key;
|
||||
const repoSlug = serverRepo.slug!;
|
||||
|
||||
const shouldExclude = (() => {
|
||||
if (config.exclude?.repos && config.exclude.repos.includes(`${projectName}/${repoSlug}`)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!config.exclude?.archived && serverRepo.archived) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!config.exclude?.forks && serverRepo.origin !== undefined) {
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
|
||||
if (shouldExclude) {
|
||||
logger.debug(`Excluding repo ${projectName}/${repoSlug} because it matches the exclude pattern`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { Settings } from "./types.js";
|
|||
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { createLogger } from "./logger.js";
|
||||
import { Redis } from 'ioredis';
|
||||
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
|
||||
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig, compileBitbucketConfig } from "./repoCompileUtils.js";
|
||||
import { BackendError, BackendException } from "@sourcebot/error";
|
||||
import { captureEvent } from "./posthog.js";
|
||||
import { env } from "./env.js";
|
||||
|
|
@ -170,6 +170,9 @@ export class ConnectionManager implements IConnectionManager {
|
|||
case 'gerrit': {
|
||||
return await compileGerritConfig(config, job.data.connectionId, orgId);
|
||||
}
|
||||
case 'bitbucket': {
|
||||
return await compileBitbucketConfig(config, job.data.connectionId, orgId, this.db);
|
||||
}
|
||||
}
|
||||
})();
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import { getGitHubReposFromConfig } from "./github.js";
|
|||
import { getGitLabReposFromConfig } from "./gitlab.js";
|
||||
import { getGiteaReposFromConfig } from "./gitea.js";
|
||||
import { getGerritReposFromConfig } from "./gerrit.js";
|
||||
import { BitbucketRepository, getBitbucketReposFromConfig } from "./bitbucket.js";
|
||||
import { SchemaRestRepository as BitbucketServerRepository } from "@coderabbitai/bitbucket/server/openapi";
|
||||
import { SchemaRepository as BitbucketCloudRepository } from "@coderabbitai/bitbucket/cloud/openapi";
|
||||
import { Prisma, PrismaClient } from '@sourcebot/db';
|
||||
import { WithRequired } from "./types.js"
|
||||
import { marshalBool } from "./utils.js";
|
||||
import { createLogger } from './logger.js';
|
||||
import { GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||
import { BitbucketConnectionConfig, GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||
import { RepoMetadata } from './types.js';
|
||||
import path from 'path';
|
||||
|
||||
|
|
@ -312,4 +315,120 @@ export const compileGerritConfig = async (
|
|||
repos: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const compileBitbucketConfig = async (
|
||||
config: BitbucketConnectionConfig,
|
||||
connectionId: number,
|
||||
orgId: number,
|
||||
db: PrismaClient) => {
|
||||
|
||||
const bitbucketReposResult = await getBitbucketReposFromConfig(config, orgId, db);
|
||||
const bitbucketRepos = bitbucketReposResult.validRepos;
|
||||
const notFound = bitbucketReposResult.notFound;
|
||||
|
||||
const hostUrl = config.url ?? 'https://bitbucket.org';
|
||||
const repoNameRoot = new URL(hostUrl)
|
||||
.toString()
|
||||
.replace(/^https?:\/\//, '');
|
||||
|
||||
const getCloneUrl = (repo: BitbucketRepository) => {
|
||||
if (!repo.links) {
|
||||
throw new Error(`No clone links found for server repo ${repo.name}`);
|
||||
}
|
||||
|
||||
// In the cloud case we simply fetch the html link and use that as the clone url. For server we
|
||||
// need to fetch the actual clone url
|
||||
if (config.deploymentType === 'cloud') {
|
||||
const htmlLink = repo.links.html as { href: string };
|
||||
return htmlLink.href;
|
||||
}
|
||||
|
||||
const cloneLinks = repo.links.clone as {
|
||||
href: string;
|
||||
name: string;
|
||||
}[];
|
||||
|
||||
for (const link of cloneLinks) {
|
||||
if (link.name === 'http') {
|
||||
return link.href;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No clone links found for repo ${repo.name}`);
|
||||
}
|
||||
|
||||
const getWebUrl = (repo: BitbucketRepository) => {
|
||||
const isServer = config.deploymentType === 'server';
|
||||
const repoLinks = (repo as BitbucketServerRepository | BitbucketCloudRepository).links;
|
||||
const repoName = isServer ? (repo as BitbucketServerRepository).name : (repo as BitbucketCloudRepository).full_name;
|
||||
|
||||
if (!repoLinks) {
|
||||
throw new Error(`No links found for ${isServer ? 'server' : 'cloud'} repo ${repoName}`);
|
||||
}
|
||||
|
||||
// In server case we get an array of lenth == 1 links in the self field, while in cloud case we get a single
|
||||
// link object in the html field
|
||||
const link = isServer ? (repoLinks.self as { name: string, href: string }[])?.[0] : repoLinks.html as { href: string };
|
||||
if (!link || !link.href) {
|
||||
throw new Error(`No ${isServer ? 'self' : 'html'} link found for ${isServer ? 'server' : 'cloud'} repo ${repoName}`);
|
||||
}
|
||||
|
||||
return link.href;
|
||||
}
|
||||
|
||||
const repos = bitbucketRepos.map((repo) => {
|
||||
const isServer = config.deploymentType === 'server';
|
||||
const codeHostType = isServer ? 'bitbucket-server' : 'bitbucket-cloud'; // zoekt expects bitbucket-server
|
||||
const displayName = isServer ? (repo as BitbucketServerRepository).name! : (repo as BitbucketCloudRepository).full_name!;
|
||||
const externalId = isServer ? (repo as BitbucketServerRepository).id!.toString() : (repo as BitbucketCloudRepository).uuid!;
|
||||
const isPublic = isServer ? (repo as BitbucketServerRepository).public : (repo as BitbucketCloudRepository).is_private === false;
|
||||
const isArchived = isServer ? (repo as BitbucketServerRepository).archived === true : false;
|
||||
const isFork = isServer ? (repo as BitbucketServerRepository).origin !== undefined : (repo as BitbucketCloudRepository).parent !== undefined;
|
||||
const repoName = path.join(repoNameRoot, displayName);
|
||||
const cloneUrl = getCloneUrl(repo);
|
||||
const webUrl = getWebUrl(repo);
|
||||
|
||||
const record: RepoData = {
|
||||
external_id: externalId,
|
||||
external_codeHostType: codeHostType,
|
||||
external_codeHostUrl: hostUrl,
|
||||
cloneUrl: cloneUrl,
|
||||
webUrl: webUrl,
|
||||
name: repoName,
|
||||
displayName: displayName,
|
||||
isFork: isFork,
|
||||
isArchived: isArchived,
|
||||
org: {
|
||||
connect: {
|
||||
id: orgId,
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
create: {
|
||||
connectionId: connectionId,
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
gitConfig: {
|
||||
'zoekt.web-url-type': codeHostType,
|
||||
'zoekt.web-url': webUrl,
|
||||
'zoekt.name': repoName,
|
||||
'zoekt.archived': marshalBool(isArchived),
|
||||
'zoekt.fork': marshalBool(isFork),
|
||||
'zoekt.public': marshalBool(isPublic),
|
||||
'zoekt.display-name': displayName,
|
||||
},
|
||||
branches: config.revisions?.branches ?? undefined,
|
||||
tags: config.revisions?.tags ?? undefined,
|
||||
} satisfies RepoMetadata,
|
||||
};
|
||||
|
||||
return record;
|
||||
})
|
||||
|
||||
return {
|
||||
repoData: repos,
|
||||
notFound,
|
||||
};
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Job, Queue, Worker } from 'bullmq';
|
|||
import { Redis } from 'ioredis';
|
||||
import { createLogger } from "./logger.js";
|
||||
import { Connection, PrismaClient, Repo, RepoToConnection, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
|
||||
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||
import { AppContext, Settings, repoMetadataSchema } from "./types.js";
|
||||
import { getRepoPath, getTokenFromConfig, measure, getShardPrefix } from "./utils.js";
|
||||
import { cloneRepository, fetchRepository, upsertGitConfig } from "./git.js";
|
||||
|
|
@ -170,31 +170,50 @@ export class RepoManager implements IRepoManager {
|
|||
// fetch the token here using the connections from the repo. Multiple connections could be referencing this repo, and each
|
||||
// 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 referrencing.
|
||||
private async getTokenForRepo(repo: RepoWithConnections, db: PrismaClient) {
|
||||
private async getAuthForRepo(repo: RepoWithConnections, db: PrismaClient): Promise<{ username: string, password: string } | undefined> {
|
||||
const repoConnections = repo.connections;
|
||||
if (repoConnections.length === 0) {
|
||||
this.logger.error(`Repo ${repo.id} has no connections`);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let username = (() => {
|
||||
switch (repo.external_codeHostType) {
|
||||
case 'gitlab':
|
||||
return 'oauth2';
|
||||
case 'bitbucket-cloud':
|
||||
case 'bitbucket-server':
|
||||
case 'github':
|
||||
case 'gitea':
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
let token: string | undefined;
|
||||
let password: string | undefined = undefined;
|
||||
for (const repoConnection of repoConnections) {
|
||||
const connection = repoConnection.connection;
|
||||
if (connection.connectionType !== 'github' && connection.connectionType !== 'gitlab' && connection.connectionType !== 'gitea') {
|
||||
if (connection.connectionType !== 'github' && connection.connectionType !== 'gitlab' && connection.connectionType !== 'gitea' && connection.connectionType !== 'bitbucket') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const config = connection.config as unknown as GithubConnectionConfig | GitlabConnectionConfig | GiteaConnectionConfig;
|
||||
const config = connection.config as unknown as GithubConnectionConfig | GitlabConnectionConfig | GiteaConnectionConfig | BitbucketConnectionConfig;
|
||||
if (config.token) {
|
||||
token = await getTokenFromConfig(config.token, connection.orgId, db, this.logger);
|
||||
if (token) {
|
||||
password = await getTokenFromConfig(config.token, connection.orgId, db, this.logger);
|
||||
if (password) {
|
||||
// If we're using a bitbucket connection we need to set the username to be able to clone the repo
|
||||
if (connection.connectionType === 'bitbucket') {
|
||||
const bitbucketConfig = config as BitbucketConnectionConfig;
|
||||
username = bitbucketConfig.user ?? "x-token-auth";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
return password
|
||||
? { username, password }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private async syncGitRepository(repo: RepoWithConnections, repoAlreadyInIndexingState: boolean) {
|
||||
|
|
@ -225,20 +244,11 @@ export class RepoManager implements IRepoManager {
|
|||
} else {
|
||||
this.logger.info(`Cloning ${repo.displayName}...`);
|
||||
|
||||
const token = await this.getTokenForRepo(repo, this.db);
|
||||
const auth = await this.getAuthForRepo(repo, this.db);
|
||||
const cloneUrl = new URL(repo.cloneUrl);
|
||||
if (token) {
|
||||
switch (repo.external_codeHostType) {
|
||||
case 'gitlab':
|
||||
cloneUrl.username = 'oauth2';
|
||||
cloneUrl.password = token;
|
||||
break;
|
||||
case 'gitea':
|
||||
case 'github':
|
||||
default:
|
||||
cloneUrl.username = token;
|
||||
break;
|
||||
}
|
||||
if (auth) {
|
||||
cloneUrl.username = auth.username;
|
||||
cloneUrl.password = auth.password;
|
||||
}
|
||||
|
||||
const { durationMs } = await measure(() => cloneRepository(cloneUrl.toString(), repoPath, ({ method, stage, progress }) => {
|
||||
|
|
@ -318,12 +328,12 @@ export class RepoManager implements IRepoManager {
|
|||
attempts++;
|
||||
this.promClient.repoIndexingReattemptsTotal.inc();
|
||||
if (attempts === maxAttempts) {
|
||||
this.logger.error(`Failed to sync repository ${repo.id} after ${maxAttempts} attempts. Error: ${error}`);
|
||||
this.logger.error(`Failed to sync repository ${repo.name} (id: ${repo.id}) after ${maxAttempts} attempts. Error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const sleepDuration = 5000 * Math.pow(2, attempts - 1);
|
||||
this.logger.error(`Failed to sync repository ${repo.id}, attempt ${attempts}/${maxAttempts}. Sleeping for ${sleepDuration / 1000}s... Error: ${error}`);
|
||||
this.logger.error(`Failed to sync repository ${repo.name} (id: ${repo.id}), attempt ${attempts}/${maxAttempts}. Sleeping for ${sleepDuration / 1000}s... Error: ${error}`);
|
||||
await new Promise(resolve => setTimeout(resolve, sleepDuration));
|
||||
}
|
||||
}
|
||||
|
|
@ -453,7 +463,7 @@ export class RepoManager implements IRepoManager {
|
|||
}
|
||||
|
||||
private async runGarbageCollectionJob(job: Job<RepoGarbageCollectionPayload>) {
|
||||
this.logger.info(`Running garbage collection job (id: ${job.id}) for repo ${job.data.repo.id}`);
|
||||
this.logger.info(`Running garbage collection job (id: ${job.id}) for repo ${job.data.repo.displayName} (id: ${job.data.repo.id})`);
|
||||
this.promClient.activeRepoGarbageCollectionJobs.inc();
|
||||
|
||||
const repo = job.data.repo as Repo;
|
||||
|
|
|
|||
179
packages/schemas/src/v3/bitbucket.schema.ts
Normal file
179
packages/schemas/src/v3/bitbucket.schema.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
|
||||
const schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "The name of the secret that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cloud",
|
||||
"server"
|
||||
],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"type": "object",
|
||||
"description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.",
|
||||
"properties": {
|
||||
"branches": {
|
||||
"type": "array",
|
||||
"description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"main",
|
||||
"release/*"
|
||||
],
|
||||
[
|
||||
"**"
|
||||
]
|
||||
],
|
||||
"default": []
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"latest",
|
||||
"v2.*.*"
|
||||
],
|
||||
[
|
||||
"**"
|
||||
]
|
||||
],
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"if": {
|
||||
"properties": {
|
||||
"deploymentType": {
|
||||
"const": "server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"additionalProperties": false
|
||||
} as const;
|
||||
export { schema as bitbucketSchema };
|
||||
76
packages/schemas/src/v3/bitbucket.type.ts
Normal file
76
packages/schemas/src/v3/bitbucket.type.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
|
||||
|
||||
export interface BitbucketConnectionConfig {
|
||||
/**
|
||||
* Bitbucket configuration
|
||||
*/
|
||||
type: "bitbucket";
|
||||
/**
|
||||
* The username to use for authentication. Only needed if token is an app password.
|
||||
*/
|
||||
user?: string;
|
||||
/**
|
||||
* An authentication token.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Bitbucket URL
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* The type of Bitbucket deployment
|
||||
*/
|
||||
deploymentType?: "cloud" | "server";
|
||||
/**
|
||||
* List of workspaces to sync. Ignored if deploymentType is server.
|
||||
*/
|
||||
workspaces?: string[];
|
||||
/**
|
||||
* List of projects to sync
|
||||
*/
|
||||
projects?: string[];
|
||||
/**
|
||||
* List of repos to sync
|
||||
*/
|
||||
repos?: string[];
|
||||
exclude?: {
|
||||
/**
|
||||
* Exclude archived repositories from syncing.
|
||||
*/
|
||||
archived?: boolean;
|
||||
/**
|
||||
* Exclude forked repositories from syncing.
|
||||
*/
|
||||
forks?: boolean;
|
||||
/**
|
||||
* List of specific repos to exclude from syncing.
|
||||
*/
|
||||
repos?: string[];
|
||||
};
|
||||
revisions?: GitRevisions;
|
||||
}
|
||||
/**
|
||||
* The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed. A maximum of 64 revisions can be indexed, with any additional revisions being ignored.
|
||||
*/
|
||||
export interface GitRevisions {
|
||||
/**
|
||||
* List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported. A maximum of 64 branches can be indexed, with any additional branches being ignored.
|
||||
*/
|
||||
branches?: string[];
|
||||
/**
|
||||
* List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported. A maximum of 64 tags can be indexed, with any additional tags being ignored.
|
||||
*/
|
||||
tags?: string[];
|
||||
}
|
||||
|
|
@ -504,6 +504,118 @@ const schema = {
|
|||
"url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"$ref": "#/oneOf/0/properties/token",
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cloud",
|
||||
"server"
|
||||
],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"$ref": "#/oneOf/0/properties/revisions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"if": {
|
||||
"properties": {
|
||||
"deploymentType": {
|
||||
"const": "server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ export type ConnectionConfig =
|
|||
| GithubConnectionConfig
|
||||
| GitlabConnectionConfig
|
||||
| GiteaConnectionConfig
|
||||
| GerritConnectionConfig;
|
||||
| GerritConnectionConfig
|
||||
| BitbucketConnectionConfig;
|
||||
|
||||
export interface GithubConnectionConfig {
|
||||
/**
|
||||
|
|
@ -235,3 +236,64 @@ export interface GerritConnectionConfig {
|
|||
projects?: string[];
|
||||
};
|
||||
}
|
||||
export interface BitbucketConnectionConfig {
|
||||
/**
|
||||
* Bitbucket configuration
|
||||
*/
|
||||
type: "bitbucket";
|
||||
/**
|
||||
* The username to use for authentication. Only needed if token is an app password.
|
||||
*/
|
||||
user?: string;
|
||||
/**
|
||||
* An authentication token.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Bitbucket URL
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* The type of Bitbucket deployment
|
||||
*/
|
||||
deploymentType?: "cloud" | "server";
|
||||
/**
|
||||
* List of workspaces to sync. Ignored if deploymentType is server.
|
||||
*/
|
||||
workspaces?: string[];
|
||||
/**
|
||||
* List of projects to sync
|
||||
*/
|
||||
projects?: string[];
|
||||
/**
|
||||
* List of repos to sync
|
||||
*/
|
||||
repos?: string[];
|
||||
exclude?: {
|
||||
/**
|
||||
* Exclude archived repositories from syncing.
|
||||
*/
|
||||
archived?: boolean;
|
||||
/**
|
||||
* Exclude forked repositories from syncing.
|
||||
*/
|
||||
forks?: boolean;
|
||||
/**
|
||||
* List of specific repos to exclude from syncing.
|
||||
*/
|
||||
repos?: string[];
|
||||
};
|
||||
revisions?: GitRevisions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -633,6 +633,118 @@ const schema = {
|
|||
"url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"$ref": "#/properties/connections/patternProperties/%5E%5Ba-zA-Z0-9_-%5D%2B%24/oneOf/0/properties/token",
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cloud",
|
||||
"server"
|
||||
],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"$ref": "#/properties/connections/patternProperties/%5E%5Ba-zA-Z0-9_-%5D%2B%24/oneOf/0/properties/revisions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"if": {
|
||||
"properties": {
|
||||
"deploymentType": {
|
||||
"const": "server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ export type ConnectionConfig =
|
|||
| GithubConnectionConfig
|
||||
| GitlabConnectionConfig
|
||||
| GiteaConnectionConfig
|
||||
| GerritConnectionConfig;
|
||||
| GerritConnectionConfig
|
||||
| BitbucketConnectionConfig;
|
||||
|
||||
export interface SourcebotConfig {
|
||||
$schema?: string;
|
||||
|
|
@ -330,3 +331,64 @@ export interface GerritConnectionConfig {
|
|||
projects?: string[];
|
||||
};
|
||||
}
|
||||
export interface BitbucketConnectionConfig {
|
||||
/**
|
||||
* Bitbucket configuration
|
||||
*/
|
||||
type: "bitbucket";
|
||||
/**
|
||||
* The username to use for authentication. Only needed if token is an app password.
|
||||
*/
|
||||
user?: string;
|
||||
/**
|
||||
* An authentication token.
|
||||
*/
|
||||
token?:
|
||||
| {
|
||||
/**
|
||||
* The name of the secret that contains the token.
|
||||
*/
|
||||
secret: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token. Only supported in declarative connection configs.
|
||||
*/
|
||||
env: string;
|
||||
};
|
||||
/**
|
||||
* Bitbucket URL
|
||||
*/
|
||||
url?: string;
|
||||
/**
|
||||
* The type of Bitbucket deployment
|
||||
*/
|
||||
deploymentType?: "cloud" | "server";
|
||||
/**
|
||||
* List of workspaces to sync. Ignored if deploymentType is server.
|
||||
*/
|
||||
workspaces?: string[];
|
||||
/**
|
||||
* List of projects to sync
|
||||
*/
|
||||
projects?: string[];
|
||||
/**
|
||||
* List of repos to sync
|
||||
*/
|
||||
repos?: string[];
|
||||
exclude?: {
|
||||
/**
|
||||
* Exclude archived repositories from syncing.
|
||||
*/
|
||||
archived?: boolean;
|
||||
/**
|
||||
* Exclude forked repositories from syncing.
|
||||
*/
|
||||
forks?: boolean;
|
||||
/**
|
||||
* List of specific repos to exclude from syncing.
|
||||
*/
|
||||
repos?: string[];
|
||||
};
|
||||
revisions?: GitRevisions;
|
||||
}
|
||||
|
|
|
|||
5
packages/web/public/bitbucket.svg
Normal file
5
packages/web/public/bitbucket.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Bitbucket" role="img"
|
||||
viewBox="0 0 512 512">
|
||||
<path fill="#2684ff" d="M422 130a10 10 0 00-9.9-11.7H100.5a10 10 0 00-10 11.7L136 409a10 10 0 009.9 8.4h221c5 0 9.2-3.5 10 -8.4L422 130zM291 316.8h-69.3l-18.7-98h104.8z"/><path fill="url(#a)" d="M59.632 25.2H40.94l-3.1 18.3h-13v18.9H52c1 0 1.7-.7 1.8-1.6l5.8-35.6z" transform="translate(89.8 85) scale(5.3285)"/><linearGradient id="a" x2="1" gradientTransform="rotate(141 22.239 22.239) scale(31.4)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></svg>
|
||||
|
After Width: | Height: | Size: 748 B |
|
|
@ -3,7 +3,7 @@
|
|||
import { env } from "@/env.mjs";
|
||||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
import { notAuthenticated, notFound, secretAlreadyExists, ServiceError, unexpectedError } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { CodeHostType, isServiceError } from "@/lib/utils";
|
||||
import { prisma } from "@/prisma";
|
||||
import { render } from "@react-email/components";
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
|
@ -27,6 +27,7 @@ import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_U
|
|||
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
|
||||
import { TenancyMode } from "./lib/types";
|
||||
import { decrementOrgSeatCount, getSubscriptionForOrg, incrementOrgSeatCount } from "./ee/features/billing/serverUtils";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
|
||||
const ajv = new Ajv({
|
||||
validateFormats: false,
|
||||
|
|
@ -442,10 +443,10 @@ export const getRepos = async (domain: string, filter: { status?: RepoIndexingSt
|
|||
}
|
||||
), /* allowSingleTenantUnauthedAccess = */ true));
|
||||
|
||||
export const createConnection = async (name: string, type: string, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
|
||||
export const createConnection = async (name: string, type: CodeHostType, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
|
||||
withAuth((session) =>
|
||||
withOrgMembership(session, domain, async ({ orgId }) => {
|
||||
const parsedConfig = parseConnectionConfig(type, connectionConfig);
|
||||
const parsedConfig = parseConnectionConfig(connectionConfig);
|
||||
if (isServiceError(parsedConfig)) {
|
||||
return parsedConfig;
|
||||
}
|
||||
|
|
@ -531,7 +532,7 @@ export const updateConnectionConfigAndScheduleSync = async (connectionId: number
|
|||
return notFound();
|
||||
}
|
||||
|
||||
const parsedConfig = parseConnectionConfig(connection.connectionType, config);
|
||||
const parsedConfig = parseConnectionConfig(config);
|
||||
if (isServiceError(parsedConfig)) {
|
||||
return parsedConfig;
|
||||
}
|
||||
|
|
@ -1154,7 +1155,7 @@ export const getSearchContexts = async (domain: string) => sew(() =>
|
|||
|
||||
////// Helpers ///////
|
||||
|
||||
const parseConnectionConfig = (connectionType: string, config: string) => {
|
||||
const parseConnectionConfig = (config: string) => {
|
||||
let parsedConfig: ConnectionConfig;
|
||||
try {
|
||||
parsedConfig = JSON.parse(config);
|
||||
|
|
@ -1166,6 +1167,7 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
|
|||
} satisfies ServiceError;
|
||||
}
|
||||
|
||||
const connectionType = parsedConfig.type;
|
||||
const schema = (() => {
|
||||
switch (connectionType) {
|
||||
case "github":
|
||||
|
|
@ -1176,6 +1178,8 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
|
|||
return giteaSchema;
|
||||
case 'gerrit':
|
||||
return gerritSchema;
|
||||
case 'bitbucket':
|
||||
return bitbucketSchema;
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
@ -1205,9 +1209,10 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
|
|||
}
|
||||
|
||||
const { numRepos, hasToken } = (() => {
|
||||
switch (parsedConfig.type) {
|
||||
switch (connectionType) {
|
||||
case "gitea":
|
||||
case "github": {
|
||||
case "github":
|
||||
case "bitbucket": {
|
||||
return {
|
||||
numRepos: parsedConfig.repos?.length,
|
||||
hasToken: !!parsedConfig.token,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const CodeHostIconButton = ({
|
|||
const captureEvent = useCaptureEvent();
|
||||
return (
|
||||
<Button
|
||||
className="flex flex-col items-center justify-center p-4 w-24 h-24 cursor-pointer gap-2"
|
||||
className="flex flex-col items-center justify-center p-4 w-36 h-36 cursor-pointer gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
captureEvent('wa_connect_code_host_button_pressed', {
|
||||
|
|
@ -29,7 +29,7 @@ export const CodeHostIconButton = ({
|
|||
}}
|
||||
>
|
||||
<Image src={logo.src} alt={name} className={cn("w-8 h-8", logo.className)} />
|
||||
<p className="text-sm font-medium">{name}</p>
|
||||
<p className="text-sm font-medium text-center">{name}</p>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use client';
|
||||
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { bitbucketCloudQuickActions } from "../../connections/quickActions";
|
||||
|
||||
interface BitbucketCloudConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0);
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
const hasWorkspaces = config.workspaces && config.workspaces.length > 0 && config.workspaces.some(w => w.trim().length > 0);
|
||||
|
||||
if (!hasProjects && !hasRepos && !hasWorkspaces) {
|
||||
return {
|
||||
message: "At least one project, repository, or workspace must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
};
|
||||
|
||||
export const BitbucketCloudConnectionCreationForm = ({ onCreated }: BitbucketCloudConnectionCreationFormProps) => {
|
||||
const defaultConfig: BitbucketConnectionConfig = {
|
||||
type: 'bitbucket',
|
||||
deploymentType: 'cloud',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<BitbucketConnectionConfig>
|
||||
type="bitbucket-cloud"
|
||||
title="Create a Bitbucket Cloud connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={bitbucketSchema}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
quickActions={bitbucketCloudQuickActions}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
'use client';
|
||||
|
||||
import SharedConnectionCreationForm from "./sharedConnectionCreationForm";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { bitbucketDataCenterQuickActions } from "../../connections/quickActions";
|
||||
|
||||
interface BitbucketDataCenterConnectionCreationFormProps {
|
||||
onCreated?: (id: number) => void;
|
||||
}
|
||||
|
||||
const additionalConfigValidation = (config: BitbucketConnectionConfig): { message: string, isValid: boolean } => {
|
||||
const hasProjects = config.projects && config.projects.length > 0 && config.projects.some(p => p.trim().length > 0);
|
||||
const hasRepos = config.repos && config.repos.length > 0 && config.repos.some(r => r.trim().length > 0);
|
||||
|
||||
if (!hasProjects && !hasRepos) {
|
||||
return {
|
||||
message: "At least one project or repository must be specified",
|
||||
isValid: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Valid",
|
||||
isValid: true,
|
||||
}
|
||||
};
|
||||
|
||||
export const BitbucketDataCenterConnectionCreationForm = ({ onCreated }: BitbucketDataCenterConnectionCreationFormProps) => {
|
||||
const defaultConfig: BitbucketConnectionConfig = {
|
||||
type: 'bitbucket',
|
||||
deploymentType: 'server',
|
||||
}
|
||||
|
||||
return (
|
||||
<SharedConnectionCreationForm<BitbucketConnectionConfig>
|
||||
type="bitbucket-server"
|
||||
title="Create a Bitbucket Data Center connection"
|
||||
defaultValues={{
|
||||
config: JSON.stringify(defaultConfig, null, 2),
|
||||
}}
|
||||
schema={bitbucketSchema}
|
||||
additionalConfigValidation={additionalConfigValidation}
|
||||
quickActions={bitbucketDataCenterQuickActions}
|
||||
onCreated={onCreated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,3 +2,5 @@ export { GitHubConnectionCreationForm } from "./githubConnectionCreationForm";
|
|||
export { GitLabConnectionCreationForm } from "./gitlabConnectionCreationForm";
|
||||
export { GiteaConnectionCreationForm } from "./giteaConnectionCreationForm";
|
||||
export { GerritConnectionCreationForm } from "./gerritConnectionCreationForm";
|
||||
export { BitbucketCloudConnectionCreationForm } from "./bitbucketCloudConnectionCreationForm";
|
||||
export { BitbucketDataCenterConnectionCreationForm } from "./bitbucketDataCenterConnectionCreationForm";
|
||||
|
|
@ -88,6 +88,10 @@ export const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHo
|
|||
return <GitHubPATCreationStep step={1} />;
|
||||
case 'gitlab':
|
||||
return <GitLabPATCreationStep step={1} />;
|
||||
case 'bitbucket-cloud':
|
||||
return <BitbucketCloudPATCreationStep step={1} />;
|
||||
case 'bitbucket-server':
|
||||
return <BitbucketServerPATCreationStep step={1} />;
|
||||
case 'gitea':
|
||||
return <GiteaPATCreationStep step={1} />;
|
||||
case 'gerrit':
|
||||
|
|
@ -179,7 +183,7 @@ export const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHo
|
|||
<FormLabel>Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="my-github-token"
|
||||
placeholder="my-access-token"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
|
@ -262,11 +266,33 @@ const GiteaPATCreationStep = ({ step }: { step: number }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const BitbucketCloudPATCreationStep = ({ step }: { step: number }) => {
|
||||
return (
|
||||
<SecretCreationStep
|
||||
step={step}
|
||||
title="Create an Access Token"
|
||||
description=<span>Please check out our <Link href="https://docs.sourcebot.dev/docs/connections/bitbucket-cloud#authenticating-with-bitbucket-cloud" target="_blank" className="underline">docs</Link> for more information on how to create auth credentials for Bitbucket Cloud.</span>
|
||||
>
|
||||
</SecretCreationStep>
|
||||
)
|
||||
}
|
||||
|
||||
const BitbucketServerPATCreationStep = ({ step }: { step: number }) => {
|
||||
return (
|
||||
<SecretCreationStep
|
||||
step={step}
|
||||
title="Create an Access Token"
|
||||
description=<span>Please check out our <Link href="https://docs.sourcebot.dev/docs/connections/bitbucket-data-center#authenticating-with-bitbucket-data-center" target="_blank" className="underline">docs</Link> for more information on how to create auth credentials for Bitbucket Data Center.</span>
|
||||
>
|
||||
</SecretCreationStep>
|
||||
)
|
||||
}
|
||||
|
||||
interface SecretCreationStepProps {
|
||||
step: number;
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SecretCreationStep = ({ step, title, description, children }: SecretCreationStepProps) => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { createZodConnectionConfigValidator } from "../../utils";
|
|||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
|
||||
import { githubQuickActions, gitlabQuickActions, giteaQuickActions, gerritQuickActions } from "../../quickActions";
|
||||
import { githubQuickActions, gitlabQuickActions, giteaQuickActions, gerritQuickActions, bitbucketCloudQuickActions, bitbucketDataCenterQuickActions } from "../../quickActions";
|
||||
import { Schema } from "ajv";
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
|
||||
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
||||
|
|
@ -27,11 +27,13 @@ import { useDomain } from "@/hooks/useDomain";
|
|||
import { SecretCombobox } from "@/app/[domain]/components/connectionCreationForms/secretCombobox";
|
||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import strings from "@/lib/strings";
|
||||
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
|
||||
|
||||
interface ConfigSettingProps {
|
||||
connectionId: number;
|
||||
config: string;
|
||||
type: string;
|
||||
type: CodeHostType;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +58,24 @@ export const ConfigSetting = (props: ConfigSettingProps) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-cloud') {
|
||||
return <ConfigSettingInternal<BitbucketConnectionConfig>
|
||||
{...props}
|
||||
type="bitbucket-cloud"
|
||||
quickActions={bitbucketCloudQuickActions}
|
||||
schema={bitbucketSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-server') {
|
||||
return <ConfigSettingInternal<BitbucketConnectionConfig>
|
||||
{...props}
|
||||
type="bitbucket-server"
|
||||
quickActions={bitbucketDataCenterQuickActions}
|
||||
schema={bitbucketSchema}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (type === 'gitea') {
|
||||
return <ConfigSettingInternal<GiteaConnectionConfig>
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ import { getOrgMembership } from "@/actions"
|
|||
import { isServiceError } from "@/lib/utils"
|
||||
import { notFound } from "next/navigation"
|
||||
import { OrgRole } from "@sourcebot/db"
|
||||
import { CodeHostType } from "@/lib/utils"
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"
|
||||
|
||||
interface ConnectionManagementPageProps {
|
||||
params: {
|
||||
domain: string
|
||||
|
|
@ -93,7 +96,7 @@ export default async function ConnectionManagementPage({ params, searchParams }:
|
|||
<DisplayNameSetting connectionId={connection.id} name={connection.name} disabled={!isOwner} />
|
||||
<ConfigSetting
|
||||
connectionId={connection.id}
|
||||
type={connection.connectionType}
|
||||
type={connection.connectionType as CodeHostType}
|
||||
config={JSON.stringify(connection.config, null, 2)}
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,18 @@ export const NewConnectionCard = ({ className, role }: NewConnectionCardProps) =
|
|||
subtitle="Cloud and Self-Hosted supported."
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
<Card
|
||||
type="bitbucket-cloud"
|
||||
title="Bitbucket Cloud"
|
||||
subtitle="Fetch repos from Bitbucket Cloud."
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
<Card
|
||||
type="bitbucket-server"
|
||||
title="Bitbucket Data Center"
|
||||
subtitle="Fetch repos from a Bitbucket DC instance."
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
<Card
|
||||
type="gitea"
|
||||
title="Gitea"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import {
|
|||
GitHubConnectionCreationForm,
|
||||
GitLabConnectionCreationForm,
|
||||
GiteaConnectionCreationForm,
|
||||
GerritConnectionCreationForm
|
||||
GerritConnectionCreationForm,
|
||||
BitbucketCloudConnectionCreationForm,
|
||||
BitbucketDataCenterConnectionCreationForm
|
||||
} from "@/app/[domain]/components/connectionCreationForms";
|
||||
import { useCallback } from "react";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
|
@ -37,5 +39,14 @@ export default function NewConnectionPage({
|
|||
return <GerritConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-cloud') {
|
||||
return <BitbucketCloudConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
if (type === 'bitbucket-server') {
|
||||
return <BitbucketDataCenterConnectionCreationForm onCreated={onCreated} />;
|
||||
}
|
||||
|
||||
|
||||
router.push(`/${domain}/connections`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
|
||||
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
|
||||
import { QuickAction } from "../components/configEditor";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -100,7 +101,7 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
|
|||
...previous,
|
||||
url: previous.url ?? "https://github.example.com",
|
||||
}),
|
||||
name: "Set a custom url",
|
||||
name: "Set url to GitHub instance",
|
||||
selectionText: "https://github.example.com",
|
||||
description: <span>Set a custom GitHub host. Defaults to <Code>https://github.com</Code>.</span>
|
||||
},
|
||||
|
|
@ -290,7 +291,7 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
|
|||
...previous,
|
||||
url: previous.url ?? "https://gitlab.example.com",
|
||||
}),
|
||||
name: "Set a custom url",
|
||||
name: "Set url to GitLab instance",
|
||||
selectionText: "https://gitlab.example.com",
|
||||
description: <span>Set a custom GitLab host. Defaults to <Code>https://gitlab.com</Code>.</span>
|
||||
},
|
||||
|
|
@ -360,7 +361,7 @@ export const giteaQuickActions: QuickAction<GiteaConnectionConfig>[] = [
|
|||
...previous,
|
||||
url: previous.url ?? "https://gitea.example.com",
|
||||
}),
|
||||
name: "Set a custom url",
|
||||
name: "Set url to Gitea instance",
|
||||
selectionText: "https://gitea.example.com",
|
||||
}
|
||||
]
|
||||
|
|
@ -390,3 +391,196 @@ export const gerritQuickActions: QuickAction<GerritConnectionConfig>[] = [
|
|||
name: "Exclude a project",
|
||||
}
|
||||
]
|
||||
|
||||
export const bitbucketCloudQuickActions: QuickAction<BitbucketConnectionConfig>[] = [
|
||||
{
|
||||
// add user
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
user: previous.user ?? "username"
|
||||
}),
|
||||
name: "Add username",
|
||||
selectionText: "username",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Username to use for authentication. This is only required if you're using an App Password (stored in <Code>token</Code>) for authentication.</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
workspaces: [
|
||||
...(previous.workspaces ?? []),
|
||||
"myWorkspace"
|
||||
]
|
||||
}),
|
||||
name: "Add a workspace",
|
||||
selectionText: "myWorkspace",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a workspace to sync with. Ensure the workspace is visible to the provided <Code>token</Code> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
repos: [
|
||||
...(previous.repos ?? []),
|
||||
"myWorkspace/myRepo"
|
||||
]
|
||||
}),
|
||||
name: "Add a repo",
|
||||
selectionText: "myWorkspace/myRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add an individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
projects: [
|
||||
...(previous.projects ?? []),
|
||||
"myProject"
|
||||
]
|
||||
}),
|
||||
name: "Add a project",
|
||||
selectionText: "myProject",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
exclude: {
|
||||
...previous.exclude,
|
||||
repos: [...(previous.exclude?.repos ?? []), "myWorkspace/myExcludedRepo"]
|
||||
}
|
||||
}),
|
||||
name: "Exclude a repo",
|
||||
selectionText: "myWorkspace/myExcludedRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Exclude a repository from syncing. Glob patterns are supported.</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
// exclude forked
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
exclude: {
|
||||
...previous.exclude,
|
||||
forks: true
|
||||
}
|
||||
}),
|
||||
name: "Exclude forked repos",
|
||||
description: <span>Exclude forked repositories from syncing.</span>
|
||||
}
|
||||
]
|
||||
|
||||
export const bitbucketDataCenterQuickActions: QuickAction<BitbucketConnectionConfig>[] = [
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
url: previous.url ?? "https://bitbucket.example.com",
|
||||
}),
|
||||
name: "Set url to Bitbucket DC instance",
|
||||
selectionText: "https://bitbucket.example.com",
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
repos: [
|
||||
...(previous.repos ?? []),
|
||||
"myProject/myRepo"
|
||||
]
|
||||
}),
|
||||
name: "Add a repo",
|
||||
selectionText: "myProject/myRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
"PROJ/repo-name",
|
||||
"MYPROJ/api"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
projects: [
|
||||
...(previous.projects ?? []),
|
||||
"myProject"
|
||||
]
|
||||
}),
|
||||
name: "Add a project",
|
||||
selectionText: "myProject",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Add a project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
exclude: {
|
||||
...previous.exclude,
|
||||
repos: [...(previous.exclude?.repos ?? []), "myProject/myExcludedRepo"]
|
||||
}
|
||||
}),
|
||||
name: "Exclude a repo",
|
||||
selectionText: "myProject/myExcludedRepo",
|
||||
description: (
|
||||
<div className="flex flex-col">
|
||||
<span>Exclude a repository from syncing. Glob patterns are supported.</span>
|
||||
<span className="text-sm mt-2 mb-1">Examples:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
"myProject/myExcludedRepo",
|
||||
"myProject2/*"
|
||||
].map((repo) => (
|
||||
<Code key={repo}>{repo}</Code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
// exclude archived
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
exclude: {
|
||||
...previous.exclude,
|
||||
archived: true
|
||||
}
|
||||
}),
|
||||
name: "Exclude archived repos",
|
||||
},
|
||||
// exclude forked
|
||||
{
|
||||
fn: (previous: BitbucketConnectionConfig) => ({
|
||||
...previous,
|
||||
exclude: {
|
||||
...previous.exclude,
|
||||
forks: true
|
||||
}
|
||||
}),
|
||||
name: "Exclude forked repos",
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import {
|
|||
GitHubConnectionCreationForm,
|
||||
GitLabConnectionCreationForm,
|
||||
GiteaConnectionCreationForm,
|
||||
GerritConnectionCreationForm
|
||||
GerritConnectionCreationForm,
|
||||
BitbucketCloudConnectionCreationForm,
|
||||
BitbucketDataCenterConnectionCreationForm
|
||||
} from "@/app/[domain]/components/connectionCreationForms";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
|
@ -79,6 +81,24 @@ export const ConnectCodeHost = ({ nextStep, securityCardEnabled }: ConnectCodeHo
|
|||
)
|
||||
}
|
||||
|
||||
if (selectedCodeHost === "bitbucket-cloud") {
|
||||
return (
|
||||
<>
|
||||
<BackButton onClick={onBack} />
|
||||
<BitbucketCloudConnectionCreationForm onCreated={onCreated} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedCodeHost === "bitbucket-server") {
|
||||
return (
|
||||
<>
|
||||
<BackButton onClick={onBack} />
|
||||
<BitbucketDataCenterConnectionCreationForm onCreated={onCreated} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +110,7 @@ const CodeHostSelection = ({ onSelect }: CodeHostSelectionProps) => {
|
|||
const captureEvent = useCaptureEvent();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 max-w-3xl mx-auto">
|
||||
<CodeHostIconButton
|
||||
name="GitHub"
|
||||
logo={getCodeHostIcon("github")!}
|
||||
|
|
@ -107,6 +127,22 @@ const CodeHostSelection = ({ onSelect }: CodeHostSelectionProps) => {
|
|||
captureEvent("wa_onboard_gitlab_selected", {});
|
||||
}}
|
||||
/>
|
||||
<CodeHostIconButton
|
||||
name="Bitbucket Cloud"
|
||||
logo={getCodeHostIcon("bitbucket-cloud")!}
|
||||
onClick={() => {
|
||||
onSelect("bitbucket-cloud");
|
||||
captureEvent("wa_onboard_bitbucket_cloud_selected", {});
|
||||
}}
|
||||
/>
|
||||
<CodeHostIconButton
|
||||
name="Bitbucket DC"
|
||||
logo={getCodeHostIcon("bitbucket-server")!}
|
||||
onClick={() => {
|
||||
onSelect("bitbucket-server");
|
||||
captureEvent("wa_onboard_bitbucket_server_selected", {});
|
||||
}}
|
||||
/>
|
||||
<CodeHostIconButton
|
||||
name="Gitea"
|
||||
logo={getCodeHostIcon("gitea")!}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,11 @@ export const CodePreviewPanel = ({
|
|||
})
|
||||
.join("/");
|
||||
|
||||
const optionalQueryParams = template.substring(template.indexOf("}}") + 2);
|
||||
const optionalQueryParams =
|
||||
template.substring(template.indexOf("}}") + 2)
|
||||
.replace("{{.Version}}", branch ?? "HEAD")
|
||||
.replace("{{.Path}}", fileMatch.FileName);
|
||||
|
||||
return url + optionalQueryParams;
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,8 @@ export type PosthogEventMap = {
|
|||
wa_onboard_gitlab_selected: {},
|
||||
wa_onboard_gitea_selected: {},
|
||||
wa_onboard_gerrit_selected: {},
|
||||
wa_onboard_bitbucket_cloud_selected: {},
|
||||
wa_onboard_bitbucket_server_selected: {},
|
||||
//////////////////////////////////////////////////////////////////
|
||||
wa_security_page_click: {},
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import githubLogo from "@/public/github.svg";
|
|||
import gitlabLogo from "@/public/gitlab.svg";
|
||||
import giteaLogo from "@/public/gitea.svg";
|
||||
import gerritLogo from "@/public/gerrit.svg";
|
||||
import bitbucketLogo from "@/public/bitbucket.svg";
|
||||
import { ServiceError } from "./serviceError";
|
||||
import { Repository, RepositoryQuery } from "./types";
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string,
|
|||
return `${path}?${queryString}`;
|
||||
}
|
||||
|
||||
export type CodeHostType = "github" | "gitlab" | "gitea" | "gerrit";
|
||||
export type CodeHostType = "github" | "gitlab" | "gitea" | "gerrit" | "bitbucket-cloud" | "bitbucket-server";
|
||||
|
||||
type CodeHostInfo = {
|
||||
type: CodeHostType;
|
||||
|
|
@ -110,6 +111,28 @@ const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: s
|
|||
iconClassName: className,
|
||||
}
|
||||
}
|
||||
case "bitbucket-server": {
|
||||
const { src, className } = getCodeHostIcon('bitbucket-server')!;
|
||||
return {
|
||||
type: "bitbucket-server",
|
||||
displayName: displayName,
|
||||
codeHostName: "Bitbucket",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
}
|
||||
case "bitbucket-cloud": {
|
||||
const { src, className } = getCodeHostIcon('bitbucket-cloud')!;
|
||||
return {
|
||||
type: "bitbucket-cloud",
|
||||
displayName: displayName,
|
||||
codeHostName: "Bitbucket",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +155,11 @@ export const getCodeHostIcon = (codeHostType: CodeHostType): { src: string, clas
|
|||
return {
|
||||
src: gerritLogo,
|
||||
}
|
||||
case "bitbucket-cloud":
|
||||
case "bitbucket-server":
|
||||
return {
|
||||
src: bitbucketLogo,
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -142,6 +170,8 @@ export const isAuthSupportedForCodeHost = (codeHostType: CodeHostType): boolean
|
|||
case "github":
|
||||
case "gitlab":
|
||||
case "gitea":
|
||||
case "bitbucket-cloud":
|
||||
case "bitbucket-server":
|
||||
return true;
|
||||
case "gerrit":
|
||||
return false;
|
||||
|
|
|
|||
105
schemas/v3/bitbucket.json
Normal file
105
schemas/v3/bitbucket.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "BitbucketConnectionConfig",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "bitbucket",
|
||||
"description": "Bitbucket configuration"
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username to use for authentication. Only needed if token is an app password."
|
||||
},
|
||||
"token": {
|
||||
"$ref": "./shared.json#/definitions/Token",
|
||||
"description": "An authentication token.",
|
||||
"examples": [
|
||||
{
|
||||
"secret": "SECRET_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "url",
|
||||
"default": "https://api.bitbucket.org/2.0",
|
||||
"description": "Bitbucket URL",
|
||||
"examples": [
|
||||
"https://bitbucket.example.com"
|
||||
],
|
||||
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
|
||||
},
|
||||
"deploymentType": {
|
||||
"type": "string",
|
||||
"enum": ["cloud", "server"],
|
||||
"default": "cloud",
|
||||
"description": "The type of Bitbucket deployment"
|
||||
},
|
||||
"workspaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of workspaces to sync. Ignored if deploymentType is server."
|
||||
},
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of projects to sync"
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of repos to sync"
|
||||
},
|
||||
"exclude": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archived": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude archived repositories from syncing."
|
||||
},
|
||||
"forks": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude forked repositories from syncing."
|
||||
},
|
||||
"repos": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"cloud_workspace/repo1",
|
||||
"server_project/repo2"
|
||||
]
|
||||
],
|
||||
"description": "List of specific repos to exclude from syncing."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"revisions": {
|
||||
"$ref": "./shared.json#/definitions/GitRevisions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"if": {
|
||||
"properties": {
|
||||
"deploymentType": { "const": "server" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["url"]
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
},
|
||||
{
|
||||
"$ref": "./gerrit.json"
|
||||
},
|
||||
{
|
||||
"$ref": "./bitbucket.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
yarn.lock
28
yarn.lock
|
|
@ -667,6 +667,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@coderabbitai/bitbucket@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "@coderabbitai/bitbucket@npm:1.1.3"
|
||||
dependencies:
|
||||
openapi-fetch: "npm:^0.13.4"
|
||||
bin:
|
||||
coderabbitai-bitbucket: dist/main.js
|
||||
checksum: 10c0/0c034866f8094b9ce68a5292d513fe6226d50b333a0a6ce929542b635a8bcf1d8d5abc7eb1aaf665ebce99e312a192d83b224085843d5641806ca4adc36ab0ff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "@colors/colors@npm:1.6.0"
|
||||
|
|
@ -11611,6 +11622,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-fetch@npm:^0.13.4":
|
||||
version: 0.13.5
|
||||
resolution: "openapi-fetch@npm:0.13.5"
|
||||
dependencies:
|
||||
openapi-typescript-helpers: "npm:^0.0.15"
|
||||
checksum: 10c0/57736d9d4310d7bc7fa5e4e37e80d28893a7fefee88ee6e0327600de893e0638479445bf0c9f5bd7b1a2429f409425d3945d6e942b23b37b8081630ac52244fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi-typescript-helpers@npm:^0.0.15":
|
||||
version: 0.0.15
|
||||
resolution: "openapi-typescript-helpers@npm:0.0.15"
|
||||
checksum: 10c0/5eb68d487b787e3e31266470b1a310726549dd45a1079655ab18066ab291b0b3c343fdf629991013706a2329b86964f8798d56ef0272b94b931fe6c19abd7a88
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"optionator@npm:^0.9.3":
|
||||
version: 0.9.4
|
||||
resolution: "optionator@npm:0.9.4"
|
||||
|
|
@ -13047,6 +13074,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "root-workspace-0b6124@workspace:."
|
||||
dependencies:
|
||||
"@coderabbitai/bitbucket": "npm:^1.1.3"
|
||||
cross-env: "npm:^7.0.3"
|
||||
dotenv-cli: "npm:^8.0.0"
|
||||
npm-run-all: "npm:^4.1.5"
|
||||
|
|
|
|||
Loading…
Reference in a new issue