mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-30 19:32:46 +08:00
feat(bitbucket): add repository webhook triggers (#6934)
* feat(bitbucket): add repository webhook triggers * fix(bitbucket): harden webhook trigger delivery * fix(bitbucket): address final trigger review * chore(bitbucket): address review conventions * fix(bitbucket): harden triggers and connector sync --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
co-authored by
Bill Leoutsakos
Waleed Latif
parent
7173a3fb37
commit
c26529a82e
@@ -12,7 +12,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, and diagnose or control pipelines. This action integration uses OAuth and does not create webhooks or triggers.
|
||||
Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, diagnose or control pipelines, and start workflows from repository and pull request events. OAuth is used for actions and automatic webhook management.
|
||||
|
||||
|
||||
|
||||
@@ -1546,3 +1546,672 @@ Read a bounded UTF-8 tail of a pipeline step log
|
||||
| `totalBytes` | number | Full log byte size when reported |
|
||||
|
||||
|
||||
|
||||
## Triggers
|
||||
|
||||
A **Trigger** is a block that starts a workflow when an event happens in this service.
|
||||
|
||||
### Bitbucket Build Status Created
|
||||
|
||||
Trigger workflow when a build status is created for a Bitbucket commit
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `commitStatus` | json | Bitbucket commit status object |
|
||||
| `commitHash` | string | Hash of the commit with this status |
|
||||
| `statusKey` | string | Key identifying the build status |
|
||||
| `statusState` | string | Current build status state |
|
||||
| `statusName` | string | Display name of the build status |
|
||||
| `statusUrl` | string | URL associated with the build status |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Build Status Updated
|
||||
|
||||
Trigger workflow when a build status is updated for a Bitbucket commit
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `commitStatus` | json | Bitbucket commit status object |
|
||||
| `commitHash` | string | Hash of the commit with this status |
|
||||
| `statusKey` | string | Key identifying the build status |
|
||||
| `statusState` | string | Current build status state |
|
||||
| `statusName` | string | Display name of the build status |
|
||||
| `statusUrl` | string | URL associated with the build status |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Commit Comment Created
|
||||
|
||||
Trigger workflow when a comment is created on a Bitbucket commit
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
| `commit` | json | Commit the comment was created on |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Approval Removed
|
||||
|
||||
Trigger workflow when approval is removed from a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `approval` | json | Pull request approval details |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Approved
|
||||
|
||||
Trigger workflow when a Bitbucket pull request is approved
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `approval` | json | Pull request approval details |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Changes Request Removed
|
||||
|
||||
Trigger workflow when a changes request is removed from a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `changesRequest` | json | Pull request changes-request details |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Changes Requested
|
||||
|
||||
Trigger workflow when changes are requested on a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `changesRequest` | json | Pull request changes-request details |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Comment Created
|
||||
|
||||
Trigger workflow when a comment is created on a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Comment Deleted
|
||||
|
||||
Trigger workflow when a comment is deleted from a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Comment Reopened
|
||||
|
||||
Trigger workflow when a resolved comment is reopened on a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Comment Resolved
|
||||
|
||||
Trigger workflow when a comment is resolved on a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Comment Updated
|
||||
|
||||
Trigger workflow when a comment is updated on a Bitbucket pull request
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
| `comment` | json | Bitbucket comment object |
|
||||
| `commentId` | number | Repository-scoped comment ID |
|
||||
| `commentContent` | string | Raw text content of the comment |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Created
|
||||
|
||||
Trigger workflow when a Bitbucket pull request is created
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Declined
|
||||
|
||||
Trigger workflow when a Bitbucket pull request is declined
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Merged
|
||||
|
||||
Trigger workflow when a Bitbucket pull request is merged
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Pull Request Updated
|
||||
|
||||
Trigger workflow when a Bitbucket pull request is updated
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `pullRequest` | json | Bitbucket pull request object |
|
||||
| `pullRequestId` | number | Repository-scoped pull request ID |
|
||||
| `pullRequestTitle` | string | Pull request title |
|
||||
| `pullRequestState` | string | Pull request state |
|
||||
| `sourceBranch` | string | Pull request source branch |
|
||||
| `destinationBranch` | string | Pull request destination branch |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Push
|
||||
|
||||
Trigger workflow when commits are pushed to a Bitbucket repository
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `push` | json | Push details, including changes and commits |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Repository Forked
|
||||
|
||||
Trigger workflow when a Bitbucket repository is forked
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `fork` | json | Newly created repository fork |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Bitbucket Repository Updated
|
||||
|
||||
Trigger workflow when a Bitbucket repository is updated
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `triggerCredentials` | string | Yes | The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required. |
|
||||
| `workspacePicker` | project-selector | Yes | The workspace containing the repository to monitor. |
|
||||
| `workspaceSlugInput` | string | Yes | Enter the workspace slug directly instead of selecting a workspace. |
|
||||
| `repositoryPicker` | project-selector | Yes | The repository where Sim creates the webhook. |
|
||||
| `repositorySlugInput` | string | Yes | Enter the repository slug directly instead of selecting a repository. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventType` | string | Bitbucket event key from X-Event-Key |
|
||||
| `hookUuid` | string | UUID of the Bitbucket repository webhook |
|
||||
| `requestUuid` | string | Bitbucket delivery request UUID |
|
||||
| `attemptNumber` | number | Bitbucket delivery attempt number |
|
||||
| `actor` | json | User or app that caused the event |
|
||||
| `repository` | json | Repository where the event occurred |
|
||||
| `payload` | json | Full parsed Bitbucket webhook payload |
|
||||
| `changes` | json | Repository fields changed by the update |
|
||||
|
||||
|
||||
@@ -150,9 +150,10 @@ The same variables also power "Sign in with Microsoft".
|
||||
For Bitbucket, create an OAuth consumer and register
|
||||
`https://<your-domain>/api/auth/oauth2/callback/bitbucket` as its callback URL. Bitbucket fixes
|
||||
permissions on the consumer instead of narrowing them per authorization request. Enable exactly
|
||||
Account read, Repositories read/write, Pull requests read/write, and Pipelines read/write
|
||||
(`account`, `repository`, `repository:write`, `pullrequest`, `pullrequest:write`, `pipeline`, and
|
||||
`pipeline:write`). Webhook permission is not required for the integration-only release.
|
||||
Account read, Repositories read/write, Pull requests read/write, Pipelines read/write, and Webhooks
|
||||
read/write (`account`, `repository`, `repository:write`, `pullrequest`, `pullrequest:write`,
|
||||
`pipeline`, `pipeline:write`, and `webhook`). The webhook permission is required for automatic
|
||||
trigger subscription management.
|
||||
|
||||
### Services with a different flow
|
||||
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BitbucketBlock, BitbucketBlockMeta } from '@/blocks/blocks/bitbucket'
|
||||
import { tools as toolRegistry } from '@/tools/registry'
|
||||
|
||||
vi.unmock('@/tools/registry')
|
||||
|
||||
const SOURCE_COMMIT_SHA = 'a'.repeat(40)
|
||||
const PIPELINE_COMMIT_SHA = 'b'.repeat(40)
|
||||
const TARGET_COMMIT_SHA = 'c'.repeat(40)
|
||||
|
||||
const EXPECTED_TOOL_IDS = [
|
||||
'bitbucket_list_workspaces',
|
||||
'bitbucket_list_repositories',
|
||||
'bitbucket_get_repository',
|
||||
'bitbucket_list_branches',
|
||||
'bitbucket_create_branch',
|
||||
'bitbucket_delete_branch',
|
||||
'bitbucket_list_commits',
|
||||
'bitbucket_get_commit',
|
||||
'bitbucket_list_directory',
|
||||
'bitbucket_get_file_metadata',
|
||||
'bitbucket_get_file',
|
||||
'bitbucket_list_pull_requests',
|
||||
'bitbucket_get_pull_request',
|
||||
'bitbucket_create_pull_request',
|
||||
'bitbucket_merge_pull_request',
|
||||
'bitbucket_get_pull_request_merge_task_status',
|
||||
'bitbucket_decline_pull_request',
|
||||
'bitbucket_approve_pull_request',
|
||||
'bitbucket_request_pull_request_changes',
|
||||
'bitbucket_get_pull_request_diff',
|
||||
'bitbucket_get_pull_request_diffstat',
|
||||
'bitbucket_list_pull_request_comments',
|
||||
'bitbucket_create_pull_request_comment',
|
||||
'bitbucket_list_pull_request_commit_statuses',
|
||||
'bitbucket_list_pipelines',
|
||||
'bitbucket_get_pipeline',
|
||||
'bitbucket_trigger_pipeline',
|
||||
'bitbucket_stop_pipeline',
|
||||
'bitbucket_list_pipeline_steps',
|
||||
'bitbucket_get_pipeline_step_log',
|
||||
] as const
|
||||
|
||||
const SAMPLE_VALUES: Record<string, unknown> = {
|
||||
oauthCredential: 'credential-1',
|
||||
workspaceSlug: 'acme',
|
||||
repoSlug: 'platform',
|
||||
pageLen: '25',
|
||||
nextUrl: 'https://api.bitbucket.org/2.0/example?page=2',
|
||||
query: 'state = "OPEN"',
|
||||
sort: '-updated_on',
|
||||
administrator: 'true',
|
||||
role: 'contributor',
|
||||
branchName: 'feature/review',
|
||||
target: SOURCE_COMMIT_SHA,
|
||||
revision: SOURCE_COMMIT_SHA,
|
||||
path: 'src/index.ts',
|
||||
state: 'OPEN',
|
||||
prId: '42',
|
||||
title: 'Improve pipeline diagnostics',
|
||||
sourceBranch: 'feature/review',
|
||||
destinationBranch: 'main',
|
||||
description: 'Adds bounded log diagnostics.',
|
||||
reviewerAccountIds: '{reviewer-a}, {reviewer-b}',
|
||||
createCloseSourceBranch: 'true',
|
||||
mergeCloseSourceBranch: 'true',
|
||||
draft: 'true',
|
||||
mergeStrategy: 'squash',
|
||||
message: 'Merge pull request 42',
|
||||
taskId: 'task-1',
|
||||
content: 'Looks good after the test fix.',
|
||||
parentId: '7',
|
||||
pipelineRefType: 'BRANCH',
|
||||
pipelineRefName: 'main',
|
||||
pipelineCommitHash: PIPELINE_COMMIT_SHA,
|
||||
pipelineSelectorType: 'BRANCH',
|
||||
pipelineSelectorPattern: 'main',
|
||||
pipelineTriggerType: 'MANUAL',
|
||||
pipelineStatus: 'FAILED',
|
||||
pipelineUuid: '{pipeline-uuid}',
|
||||
targetRef: 'main',
|
||||
targetCommitHash: TARGET_COMMIT_SHA,
|
||||
stepUuid: '{step-uuid}',
|
||||
maxCharacters: '65536',
|
||||
}
|
||||
|
||||
type BitbucketOperation = (typeof EXPECTED_TOOL_IDS)[number]
|
||||
|
||||
function mapBlockParams(
|
||||
operation: BitbucketOperation,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Record<string, unknown> {
|
||||
const mapper = BitbucketBlock.tools.config?.params
|
||||
if (!mapper) throw new Error('Bitbucket block is missing tools.config.params')
|
||||
return mapper({ operation, ...SAMPLE_VALUES, ...overrides }) as Record<string, unknown>
|
||||
}
|
||||
|
||||
function buildRequestUrl(
|
||||
operation: BitbucketOperation,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): string {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation })
|
||||
if (!toolId) throw new Error(`Bitbucket operation ${operation} did not resolve to a tool`)
|
||||
const url = toolRegistry[toolId].request.url
|
||||
if (typeof url !== 'function') return url
|
||||
return url({ ...mapBlockParams(operation, overrides), accessToken: 'oauth-token' } as never)
|
||||
}
|
||||
|
||||
function buildRequestBody(
|
||||
operation: BitbucketOperation,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): unknown {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation })
|
||||
if (!toolId) throw new Error(`Bitbucket operation ${operation} did not resolve to a tool`)
|
||||
const body = toolRegistry[toolId].request.body
|
||||
if (!body) throw new Error(`Bitbucket tool ${toolId} is missing request.body`)
|
||||
return body({ ...mapBlockParams(operation, overrides), accessToken: 'oauth-token' } as never)
|
||||
}
|
||||
|
||||
describe('BitbucketBlock', () => {
|
||||
it('exposes every Bitbucket action one-to-one through the operation dropdown', () => {
|
||||
const operation = BitbucketBlock.subBlocks.find((subBlock) => subBlock.id === 'operation')
|
||||
const options =
|
||||
typeof operation?.options === 'function' ? operation.options() : operation?.options
|
||||
|
||||
expect(options?.map((option) => option.id).sort()).toEqual([...EXPECTED_TOOL_IDS].sort())
|
||||
expect([...BitbucketBlock.tools.access].sort()).toEqual([...EXPECTED_TOOL_IDS].sort())
|
||||
expect(new Set(options?.map((option) => option.id)).size).toBe(EXPECTED_TOOL_IDS.length)
|
||||
})
|
||||
|
||||
it.each(EXPECTED_TOOL_IDS)('%s resolves to a registered tool', (operation) => {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation })
|
||||
expect(toolId).toBe(operation)
|
||||
expect(toolRegistry[toolId]).toBeDefined()
|
||||
})
|
||||
|
||||
it.each(EXPECTED_TOOL_IDS)('%s supplies every required user-facing tool param', (operation) => {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string
|
||||
const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {}
|
||||
const required = Object.entries(toolRegistry[toolId].params ?? {})
|
||||
.filter(([id, config]) => config.required && id !== 'accessToken')
|
||||
.map(([id]) => id)
|
||||
|
||||
expect(required.filter((id) => mapped[id] === undefined || mapped[id] === '')).toEqual([])
|
||||
})
|
||||
|
||||
it.each(EXPECTED_TOOL_IDS)('%s maps every non-hidden tool parameter', (operation) => {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string
|
||||
const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {}
|
||||
const exposed = Object.entries(toolRegistry[toolId].params ?? {})
|
||||
.filter(([id, config]) => id !== 'accessToken' && config.visibility !== 'hidden')
|
||||
.map(([id]) => id)
|
||||
|
||||
expect(exposed.filter((id) => mapped[id] === undefined)).toEqual([])
|
||||
})
|
||||
|
||||
it.each(EXPECTED_TOOL_IDS)('%s does not emit unsupported execution params', (operation) => {
|
||||
const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string
|
||||
const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {}
|
||||
const accepted = new Set([...Object.keys(toolRegistry[toolId].params ?? {}), 'oauthCredential'])
|
||||
|
||||
expect(Object.keys(mapped).filter((key) => !accepted.has(key))).toEqual([])
|
||||
})
|
||||
|
||||
it.each(EXPECTED_TOOL_IDS)('%s exposes every tool output through the block', (operation) => {
|
||||
const toolOutputs = Object.keys(toolRegistry[operation].outputs ?? {})
|
||||
const blockOutputs = new Set(Object.keys(BitbucketBlock.outputs))
|
||||
|
||||
expect(toolOutputs.filter((key) => !blockOutputs.has(key))).toEqual([])
|
||||
})
|
||||
|
||||
it('uses canonical picker/manual pairs without colliding with visual subblock IDs', () => {
|
||||
const expectedPairs = {
|
||||
oauthCredential: ['accountPicker', 'credentialIdInput'],
|
||||
workspaceSlug: ['workspacePicker', 'workspaceSlugInput'],
|
||||
repoSlug: ['repositoryPicker', 'repositorySlugInput'],
|
||||
}
|
||||
|
||||
for (const [canonicalParamId, ids] of Object.entries(expectedPairs)) {
|
||||
const members = BitbucketBlock.subBlocks.filter(
|
||||
(subBlock) => subBlock.canonicalParamId === canonicalParamId
|
||||
)
|
||||
expect(members.map((member) => member.id).sort()).toEqual([...ids].sort())
|
||||
expect(members.filter((member) => member.mode === 'basic')).toHaveLength(1)
|
||||
expect(members.filter((member) => member.mode === 'advanced')).toHaveLength(1)
|
||||
expect(members[0].required).toEqual(members[1].required)
|
||||
expect(members[0].condition).toEqual(members[1].condition)
|
||||
expect(BitbucketBlock.subBlocks.some((member) => member.id === canonicalParamId)).toBe(false)
|
||||
}
|
||||
|
||||
expect(
|
||||
BitbucketBlock.subBlocks.find((member) => member.id === 'workspacePicker')?.dependsOn
|
||||
).toEqual(['accountPicker'])
|
||||
expect(
|
||||
BitbucketBlock.subBlocks.find((member) => member.id === 'workspaceSlugInput')?.dependsOn
|
||||
).toEqual(['credentialIdInput'])
|
||||
expect(
|
||||
BitbucketBlock.subBlocks.find((member) => member.id === 'repositoryPicker')?.dependsOn
|
||||
).toEqual(['accountPicker', 'workspacePicker'])
|
||||
expect(
|
||||
BitbucketBlock.subBlocks.find((member) => member.id === 'repositorySlugInput')?.dependsOn
|
||||
).toEqual(['credentialIdInput', 'workspaceSlugInput'])
|
||||
})
|
||||
|
||||
it('coerces execution values and fixes pipeline triggering to a branch ref target', () => {
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_create_pull_request',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({
|
||||
reviewerUuids: ['{reviewer-a}', '{reviewer-b}'],
|
||||
closeSourceBranch: true,
|
||||
})
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_trigger_pipeline',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({ refType: 'branch', refName: 'main' })
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_list_repositories',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({ pageLen: 25 })
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_get_pull_request_diff',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({ prId: 42, path: 'src/index.ts', maxCharacters: 65536 })
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_get_file_metadata',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({ commit: SOURCE_COMMIT_SHA, path: 'src/index.ts' })
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_get_pull_request_merge_task_status',
|
||||
...SAMPLE_VALUES,
|
||||
})
|
||||
).toMatchObject({ prId: 42, taskId: 'task-1' })
|
||||
})
|
||||
|
||||
it.each([undefined, null, '', ' '])(
|
||||
'treats %p as omission across optional execution value kinds',
|
||||
(value) => {
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request_comment', { parentId: value }).parentId
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request', { draft: value }).draft
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request', { description: value }).description
|
||||
).toBeUndefined()
|
||||
expect(mapBlockParams('bitbucket_list_branches', { query: value }).q).toBeUndefined()
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds: value }).reviewerUuids
|
||||
).toBeUndefined()
|
||||
}
|
||||
)
|
||||
|
||||
it('coerces canonical positive integer strings and reaches the real tool URL', () => {
|
||||
expect(buildRequestUrl('bitbucket_decline_pull_request', { prId: '42' })).toMatch(
|
||||
/\/pullrequests\/42\/decline$/
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
true,
|
||||
false,
|
||||
[7],
|
||||
{ value: 7 },
|
||||
0,
|
||||
1.5,
|
||||
Number.MAX_SAFE_INTEGER + 1,
|
||||
'false',
|
||||
' 42 ',
|
||||
'01',
|
||||
'7.0',
|
||||
'1e2',
|
||||
'-1',
|
||||
])('rejects malformed PR ids before a mutation request is built: %p', (prId) => {
|
||||
expect(() => buildRequestUrl('bitbucket_decline_pull_request', { prId })).toThrow(/prId/)
|
||||
})
|
||||
|
||||
it('does not let a create-pull-request close setting reach a merge', () => {
|
||||
const merged = buildRequestBody('bitbucket_merge_pull_request', {
|
||||
createCloseSourceBranch: 'true',
|
||||
mergeCloseSourceBranch: '',
|
||||
}) as Record<string, unknown>
|
||||
expect(merged).not.toHaveProperty('close_source_branch')
|
||||
|
||||
const created = buildRequestBody('bitbucket_create_pull_request', {
|
||||
createCloseSourceBranch: '',
|
||||
mergeCloseSourceBranch: 'true',
|
||||
}) as Record<string, unknown>
|
||||
expect(created).not.toHaveProperty('close_source_branch')
|
||||
})
|
||||
|
||||
it('does not turn a malformed optional parent ID into a top-level comment', () => {
|
||||
expect(() =>
|
||||
buildRequestBody('bitbucket_create_pull_request_comment', { parentId: { id: 7 } })
|
||||
).toThrow(/parentId/)
|
||||
})
|
||||
|
||||
it('preserves explicit false booleans and rejects other present boolean shapes', () => {
|
||||
expect(
|
||||
buildRequestBody('bitbucket_create_pull_request', {
|
||||
createCloseSourceBranch: false,
|
||||
draft: 'false',
|
||||
})
|
||||
).toMatchObject({ close_source_branch: false, draft: false })
|
||||
|
||||
for (const draft of [0, 1, 'yes', ' false ', [], {}]) {
|
||||
expect(
|
||||
() => mapBlockParams('bitbucket_create_pull_request', { draft }),
|
||||
String(draft)
|
||||
).toThrow(/draft/)
|
||||
}
|
||||
})
|
||||
|
||||
it('validates reviewer lists atomically instead of filtering malformed elements', () => {
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request', {
|
||||
reviewerAccountIds: [' {reviewer-a} ', '{reviewer-b}'],
|
||||
}).reviewerUuids
|
||||
).toEqual(['{reviewer-a}', '{reviewer-b}'])
|
||||
expect(
|
||||
mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds: [] }).reviewerUuids
|
||||
).toEqual([])
|
||||
|
||||
for (const reviewerAccountIds of [
|
||||
['{reviewer-a}', 7],
|
||||
['{reviewer-a}', ' '],
|
||||
'{reviewer-a},, {reviewer-b}',
|
||||
{ uuid: '{reviewer-a}' },
|
||||
]) {
|
||||
expect(
|
||||
() => mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds }),
|
||||
JSON.stringify(reviewerAccountIds)
|
||||
).toThrow(/reviewerAccountIds/)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects malformed present string values instead of silently omitting them', () => {
|
||||
expect(() => mapBlockParams('bitbucket_list_branches', { query: false })).toThrow(/query/)
|
||||
expect(() => mapBlockParams('bitbucket_create_pull_request', { description: {} })).toThrow(
|
||||
/description/
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves meaningful whitespace in repository paths and authored text', () => {
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_get_file',
|
||||
...SAMPLE_VALUES,
|
||||
path: ' docs/release notes.md ',
|
||||
})
|
||||
).toMatchObject({ path: ' docs/release notes.md ' })
|
||||
|
||||
expect(
|
||||
BitbucketBlock.tools.config?.params?.({
|
||||
operation: 'bitbucket_create_pull_request_comment',
|
||||
...SAMPLE_VALUES,
|
||||
content: ' indented Markdown\n',
|
||||
})
|
||||
).toMatchObject({ content: ' indented Markdown\n' })
|
||||
})
|
||||
|
||||
it('covers every action with a canvas sentence and declares no trigger support', () => {
|
||||
const sentences = BitbucketBlock.canvasPresentation?.sentences?.byOperation ?? {}
|
||||
expect(Object.keys(sentences).sort()).toEqual([...EXPECTED_TOOL_IDS].sort())
|
||||
expect(BitbucketBlock.triggerAllowed).toBeUndefined()
|
||||
expect(BitbucketBlock.triggers).toBeUndefined()
|
||||
expect(BitbucketBlockMeta.tags).not.toContain('webhooks')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig, BlockMeta } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
const WORKSPACE_FIELD = ['workspacePicker', 'workspaceSlugInput'] as const
|
||||
const REPOSITORY_FIELD = ['repositoryPicker', 'repositorySlugInput'] as const
|
||||
@@ -146,11 +146,12 @@ export const BitbucketBlock: BlockConfig = {
|
||||
name: 'Bitbucket',
|
||||
description: 'Work with Bitbucket Cloud repositories, pull requests, and pipelines',
|
||||
longDescription:
|
||||
'Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, and diagnose or control pipelines. This action integration uses OAuth and does not create webhooks or triggers.',
|
||||
'Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, diagnose or control pipelines, and start workflows from repository and pull request events. OAuth is used for actions and automatic webhook management.',
|
||||
docsLink: 'https://docs.sim.ai/integrations/bitbucket',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DevOps,
|
||||
authMode: AuthMode.OAuth,
|
||||
triggerAllowed: true,
|
||||
bgColor: '#FFFFFF',
|
||||
iconColor: '#2684FF',
|
||||
icon: BitbucketIcon,
|
||||
@@ -317,7 +318,15 @@ export const BitbucketBlock: BlockConfig = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'bitbucket',
|
||||
requiredScopes: getScopesForService('bitbucket'),
|
||||
requiredScopes: [
|
||||
'account',
|
||||
'repository',
|
||||
'repository:write',
|
||||
'pullrequest',
|
||||
'pullrequest:write',
|
||||
'pipeline',
|
||||
'pipeline:write',
|
||||
],
|
||||
placeholder: 'Select Bitbucket account',
|
||||
required: true,
|
||||
},
|
||||
@@ -811,6 +820,25 @@ export const BitbucketBlock: BlockConfig = {
|
||||
},
|
||||
placeholder: 'Maximum bounded content to return',
|
||||
},
|
||||
...getTrigger('bitbucket_push').subBlocks,
|
||||
...getTrigger('bitbucket_repository_forked').subBlocks,
|
||||
...getTrigger('bitbucket_repository_updated').subBlocks,
|
||||
...getTrigger('bitbucket_commit_comment_created').subBlocks,
|
||||
...getTrigger('bitbucket_build_status_created').subBlocks,
|
||||
...getTrigger('bitbucket_build_status_updated').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_created').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_updated').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_approved').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_approval_removed').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_changes_requested').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_changes_request_removed').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_merged').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_declined').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_comment_created').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_comment_updated').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_comment_deleted').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_comment_resolved').subBlocks,
|
||||
...getTrigger('bitbucket_pull_request_comment_reopened').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
// Keep this list literal so the documentation generator can statically discover every action.
|
||||
@@ -1122,6 +1150,30 @@ export const BitbucketBlock: BlockConfig = {
|
||||
log: { type: 'string', description: 'Bounded pipeline step log tail' },
|
||||
totalBytes: { type: 'number', description: 'Full pipeline log byte size when reported' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'bitbucket_push',
|
||||
'bitbucket_repository_forked',
|
||||
'bitbucket_repository_updated',
|
||||
'bitbucket_commit_comment_created',
|
||||
'bitbucket_build_status_created',
|
||||
'bitbucket_build_status_updated',
|
||||
'bitbucket_pull_request_created',
|
||||
'bitbucket_pull_request_updated',
|
||||
'bitbucket_pull_request_approved',
|
||||
'bitbucket_pull_request_approval_removed',
|
||||
'bitbucket_pull_request_changes_requested',
|
||||
'bitbucket_pull_request_changes_request_removed',
|
||||
'bitbucket_pull_request_merged',
|
||||
'bitbucket_pull_request_declined',
|
||||
'bitbucket_pull_request_comment_created',
|
||||
'bitbucket_pull_request_comment_updated',
|
||||
'bitbucket_pull_request_comment_deleted',
|
||||
'bitbucket_pull_request_comment_resolved',
|
||||
'bitbucket_pull_request_comment_reopened',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const BitbucketBlockMeta = {
|
||||
|
||||
@@ -31,7 +31,13 @@ const REPOSITORY = {
|
||||
}
|
||||
|
||||
function fileEntry(path: string, size = 10, attributes: string[] = []) {
|
||||
return { type: 'commit_file', path, size, attributes, commit: { hash: COMMIT } }
|
||||
return {
|
||||
type: 'commit_file',
|
||||
path,
|
||||
size,
|
||||
attributes,
|
||||
commit: { hash: COMMIT },
|
||||
}
|
||||
}
|
||||
|
||||
function dirEntry(path: string) {
|
||||
@@ -43,11 +49,17 @@ function pullRequestFixture(id: number, overrides: Record<string, unknown> = {})
|
||||
id,
|
||||
title: `PR ${id}`,
|
||||
state: 'OPEN',
|
||||
summary: { raw: `Body of ${id}`, markup: 'markdown', html: `<p>Body of ${id}</p>` },
|
||||
summary: {
|
||||
raw: `Body of ${id}`,
|
||||
markup: 'markdown',
|
||||
html: `<p>Body of ${id}</p>`,
|
||||
},
|
||||
author: { display_name: 'Ada Lovelace', nickname: 'ada' },
|
||||
created_on: '2026-01-01T00:00:00.000000+00:00',
|
||||
updated_on: '2026-02-01T00:00:00.000000+00:00',
|
||||
links: { html: { href: `https://bitbucket.org/acme/widgets/pull-requests/${id}` } },
|
||||
links: {
|
||||
html: { href: `https://bitbucket.org/acme/widgets/pull-requests/${id}` },
|
||||
},
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -77,6 +89,14 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('bitbucket repository file listing', () => {
|
||||
it('uses the least OAuth scope that covers both pull requests and repository reads', () => {
|
||||
expect(bitbucketConnector.auth).toMatchObject({
|
||||
mode: 'oauth',
|
||||
provider: 'bitbucket',
|
||||
requiredScopes: ['pullrequest'],
|
||||
})
|
||||
})
|
||||
|
||||
it('lists text files as deferred stubs and drops binary, symlink, and submodule entries', async () => {
|
||||
mockApi([
|
||||
[
|
||||
@@ -171,7 +191,10 @@ describe('bitbucket repository file listing', () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\/[a-f0-9]+\/\?/,
|
||||
() => jsonResponse({ values: [dirEntry('a/b/c/d/e'), fileEntry('root.md')] }),
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [dirEntry('a/b/c/d/e'), fileEntry('root.md')],
|
||||
}),
|
||||
],
|
||||
[
|
||||
/\/src\/[a-f0-9]+\/a\/b\/c\/d\/e\/\?/,
|
||||
@@ -201,8 +224,7 @@ describe('bitbucket repository file listing', () => {
|
||||
})
|
||||
|
||||
it('replays the opaque next link verbatim instead of rebuilding it', async () => {
|
||||
const nextUrl =
|
||||
'https://api.bitbucket.org/2.0/repositories/acme/widgets/src/abc/?page=2&opaque=xyz'
|
||||
const nextUrl = `https://api.bitbucket.org/2.0/repositories/acme/widgets/src/${COMMIT}/?page=2&opaque=xyz`
|
||||
mockApi([
|
||||
[/page=2/, () => jsonResponse({ values: [fileEntry('second.md')] })],
|
||||
[/\/src\//, () => jsonResponse({ values: [fileEntry('first.md')], next: nextUrl })],
|
||||
@@ -226,7 +248,11 @@ describe('bitbucket repository file listing', () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\//,
|
||||
() => jsonResponse({ values: [], next: 'https://evil.example.com/2.0/repositories/x' }),
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [],
|
||||
next: 'https://evil.example.com/2.0/repositories/x',
|
||||
}),
|
||||
],
|
||||
])
|
||||
|
||||
@@ -242,6 +268,55 @@ describe('bitbucket repository file listing', () => {
|
||||
bitbucketConnector.listDocuments(ACCESS_TOKEN, CONFIG, first.nextCursor, syncContext)
|
||||
).rejects.toThrow(/Bitbucket Cloud API/)
|
||||
})
|
||||
|
||||
it('rejects a next cursor for another Bitbucket repository', async () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\//,
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [],
|
||||
next: `https://api.bitbucket.org/2.0/repositories/other/repo/src/${COMMIT}/?page=2`,
|
||||
}),
|
||||
],
|
||||
])
|
||||
|
||||
const syncContext: Record<string, unknown> = {}
|
||||
const first = await bitbucketConnector.listDocuments(
|
||||
ACCESS_TOKEN,
|
||||
CONFIG,
|
||||
undefined,
|
||||
syncContext
|
||||
)
|
||||
|
||||
await expect(
|
||||
bitbucketConnector.listDocuments(ACCESS_TOKEN, CONFIG, first.nextCursor, syncContext)
|
||||
).rejects.toThrow(/does not belong/)
|
||||
})
|
||||
|
||||
it('rejects a malformed source pagination envelope', async () => {
|
||||
mockApi([[/\/src\//, () => jsonResponse({ next: null })]])
|
||||
|
||||
await expect(
|
||||
bitbucketConnector.listDocuments(ACCESS_TOKEN, CONFIG, undefined, {})
|
||||
).rejects.toThrow(/values array/)
|
||||
})
|
||||
|
||||
it('blocks reconciliation when a source entry lacks its documented path', async () => {
|
||||
mockApi([[/\/src\//, () => jsonResponse({ values: [{ type: 'commit_file' }] })]])
|
||||
|
||||
const syncContext: Record<string, unknown> = {}
|
||||
const result = await bitbucketConnector.listDocuments(
|
||||
ACCESS_TOKEN,
|
||||
CONFIG,
|
||||
undefined,
|
||||
syncContext
|
||||
)
|
||||
|
||||
expect(result.documents).toEqual([])
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
expect(syncContext.listingTruncated).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bitbucket source listing timeouts', () => {
|
||||
@@ -278,13 +353,13 @@ describe('bitbucket source listing timeouts', () => {
|
||||
)
|
||||
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
expect(syncContext.listingTruncated).toBe(true)
|
||||
expect(result.documents).toEqual([])
|
||||
expect(requestedUrls(/\/src\//)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('flags the listing capped when a replayed next cursor times out, without re-cutting it', async () => {
|
||||
const nextUrl =
|
||||
'https://api.bitbucket.org/2.0/repositories/acme/widgets/src/abc/?page=2&max_depth=5'
|
||||
const nextUrl = `https://api.bitbucket.org/2.0/repositories/acme/widgets/src/${COMMIT}/?page=2&max_depth=5`
|
||||
mockApi([
|
||||
[/page=2/, () => new Response('timeout', { status: 555 })],
|
||||
[/\/src\//, () => jsonResponse({ values: [fileEntry('first.md')], next: nextUrl })],
|
||||
@@ -305,12 +380,13 @@ describe('bitbucket source listing timeouts', () => {
|
||||
)
|
||||
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
expect(syncContext.listingTruncated).toBe(true)
|
||||
expect(second.documents).toEqual([])
|
||||
/** Exactly one attempt: an opaque cursor is never rebuilt at another depth. */
|
||||
expect(requestedUrls(/page=2/)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('leaves the listing reconcilable when a directory is simply absent', async () => {
|
||||
it('blocks reconciliation when a pinned source directory is unexpectedly absent', async () => {
|
||||
mockApi([[/\/src\//, () => jsonResponse({ type: 'error' }, 404)]])
|
||||
|
||||
const syncContext: Record<string, unknown> = {}
|
||||
@@ -321,17 +397,17 @@ describe('bitbucket source listing timeouts', () => {
|
||||
syncContext
|
||||
)
|
||||
|
||||
expect(syncContext.listingCapped).toBeUndefined()
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
expect(syncContext.listingTruncated).toBe(true)
|
||||
expect(result.hasMore).toBe(false)
|
||||
})
|
||||
|
||||
it('flags the listing capped when the token stops working mid-walk', async () => {
|
||||
it('fails the sync when the token stops working mid-walk', async () => {
|
||||
mockApi([[/\/src\//, () => jsonResponse({ type: 'error' }, 403)]])
|
||||
|
||||
const syncContext: Record<string, unknown> = {}
|
||||
await bitbucketConnector.listDocuments(ACCESS_TOKEN, CONFIG, undefined, syncContext)
|
||||
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
await expect(
|
||||
bitbucketConnector.listDocuments(ACCESS_TOKEN, CONFIG, undefined, {})
|
||||
).rejects.toThrow(/authorization failed.*403/i)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -340,7 +416,10 @@ describe('bitbucket maxItems cap', () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\//,
|
||||
() => jsonResponse({ values: [fileEntry('a.md'), fileEntry('b.md'), fileEntry('c.md')] }),
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [fileEntry('a.md'), fileEntry('b.md'), fileEntry('c.md')],
|
||||
}),
|
||||
],
|
||||
])
|
||||
|
||||
@@ -395,7 +474,7 @@ describe('bitbucket maxItems cap', () => {
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [fileEntry('a.md'), fileEntry('b.md')],
|
||||
next: 'https://api.bitbucket.org/2.0/repositories/acme/widgets/src/abc/?page=2',
|
||||
next: `https://api.bitbucket.org/2.0/repositories/acme/widgets/src/${COMMIT}/?page=2`,
|
||||
}),
|
||||
],
|
||||
])
|
||||
@@ -463,7 +542,7 @@ describe('bitbucket pull request listing', () => {
|
||||
const url = requestedUrls(/\/pullrequests/)[0]
|
||||
expect(url).toContain('pagelen=50')
|
||||
/** `+` must reach Bitbucket percent-encoded or it is read as a space. */
|
||||
expect(url).toContain('fields=%2Bvalues.summary')
|
||||
expect(url).toContain('fields=%2Bvalues.rendered.description')
|
||||
expect(url).toContain('sort=-id')
|
||||
expect(url).toContain('state=OPEN')
|
||||
})
|
||||
@@ -500,17 +579,20 @@ describe('bitbucket pull request listing', () => {
|
||||
expect(url.searchParams.get('q')).toBe('updated_on > 2026-02-03T04:05:06.007+00:00')
|
||||
})
|
||||
|
||||
it('maps the author-typed body and falls back to the rendered description', async () => {
|
||||
it('prefers the rendered description and falls back to the summary', async () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/pullrequests/,
|
||||
() =>
|
||||
jsonResponse({
|
||||
values: [
|
||||
pullRequestFixture(7),
|
||||
pullRequestFixture(7, {
|
||||
summary: { raw: 'Summary value' },
|
||||
rendered: { description: { raw: 'Rendered pull request body' } },
|
||||
}),
|
||||
pullRequestFixture(8, {
|
||||
summary: undefined,
|
||||
rendered: { description: { raw: 'Rendered body' } },
|
||||
summary: { raw: 'Summary fallback' },
|
||||
rendered: undefined,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -519,12 +601,28 @@ describe('bitbucket pull request listing', () => {
|
||||
|
||||
const result = await bitbucketConnector.listDocuments(ACCESS_TOKEN, PR_CONFIG, undefined, {})
|
||||
|
||||
expect(result.documents[0].content).toBe('PR 7\n\nBody of 7')
|
||||
expect(result.documents[0].content).toBe('PR 7\n\nRendered pull request body')
|
||||
expect(result.documents[0].contentDeferred).toBe(false)
|
||||
expect(result.documents[0].contentHash).toBe(
|
||||
'bitbucket:pr:acme/widgets:7:2026-02-01T00:00:00.000000+00:00'
|
||||
)
|
||||
expect(result.documents[1].content).toBe('PR 8\n\nRendered body')
|
||||
expect(result.documents[1].content).toBe('PR 8\n\nSummary fallback')
|
||||
})
|
||||
|
||||
it('blocks reconciliation when a pull request record has no documented ID', async () => {
|
||||
mockApi([[/\/pullrequests/, () => jsonResponse({ values: [{ title: 'Malformed PR' }] })]])
|
||||
|
||||
const syncContext: Record<string, unknown> = {}
|
||||
const result = await bitbucketConnector.listDocuments(
|
||||
ACCESS_TOKEN,
|
||||
PR_CONFIG,
|
||||
undefined,
|
||||
syncContext
|
||||
)
|
||||
|
||||
expect(result.documents).toEqual([])
|
||||
expect(syncContext.listingCapped).toBe(true)
|
||||
expect(syncContext.listingTruncated).toBe(true)
|
||||
})
|
||||
|
||||
it('walks from the code phase into the pull request phase for combined content', async () => {
|
||||
@@ -599,6 +697,19 @@ describe('bitbucket getDocument', () => {
|
||||
expect(doc?.content).toBe('')
|
||||
})
|
||||
|
||||
it('does not misclassify an undocumented redirect as Git LFS', async () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\/[a-f0-9]+\/moved\.txt$/,
|
||||
() => new Response(null, { status: 302, headers: { Location: '/unexpected' } }),
|
||||
],
|
||||
])
|
||||
|
||||
await expect(
|
||||
bitbucketConnector.getDocument(ACCESS_TOKEN, CONFIG, 'file:moved.txt', {})
|
||||
).rejects.toThrow(/302/)
|
||||
})
|
||||
|
||||
it('surfaces a file with a NUL byte as a skipped binary', async () => {
|
||||
mockApi([
|
||||
[
|
||||
@@ -612,6 +723,20 @@ describe('bitbucket getDocument', () => {
|
||||
expect(doc?.skippedReason).toMatch(/Binary/)
|
||||
})
|
||||
|
||||
it('surfaces non-UTF-8 source as skipped instead of indexing replacement characters', async () => {
|
||||
mockApi([
|
||||
[
|
||||
/\/src\/[a-f0-9]+\/latin1\.txt$/,
|
||||
() => new Response(Buffer.from([0x63, 0x61, 0x66, 0xe9]), { status: 200 }),
|
||||
],
|
||||
])
|
||||
|
||||
const doc = await bitbucketConnector.getDocument(ACCESS_TOKEN, CONFIG, 'file:latin1.txt', {})
|
||||
|
||||
expect(doc?.skippedReason).toMatch(/Non-UTF-8/)
|
||||
expect(doc?.content).toBe('')
|
||||
})
|
||||
|
||||
it('returns null for a file the ref no longer carries', async () => {
|
||||
mockApi([[/\/src\/[a-f0-9]+\/gone\.md$/, () => jsonResponse({ type: 'error' }, 404)]])
|
||||
|
||||
@@ -634,6 +759,14 @@ describe('bitbucket getDocument', () => {
|
||||
expect(await bitbucketConnector.getDocument(ACCESS_TOKEN, CONFIG, 'pr:9', {})).toBeNull()
|
||||
})
|
||||
|
||||
it('throws when a successful pull request response lacks its documented ID', async () => {
|
||||
mockApi([[/\/pullrequests\/9$/, () => jsonResponse({ title: 'Malformed PR' })]])
|
||||
|
||||
await expect(bitbucketConnector.getDocument(ACCESS_TOKEN, CONFIG, 'pr:9', {})).rejects.toThrow(
|
||||
/response was malformed/
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects an externalId with no recognized resource prefix', async () => {
|
||||
mockApi([])
|
||||
|
||||
@@ -704,7 +837,10 @@ describe('bitbucket validateConfig', () => {
|
||||
])
|
||||
|
||||
expect(
|
||||
await bitbucketConnector.validateConfig(ACCESS_TOKEN, { ...CONFIG, ref: 'v1.0' })
|
||||
await bitbucketConnector.validateConfig(ACCESS_TOKEN, {
|
||||
...CONFIG,
|
||||
ref: 'v1.0',
|
||||
})
|
||||
).toEqual({ valid: true })
|
||||
})
|
||||
|
||||
@@ -712,7 +848,21 @@ describe('bitbucket validateConfig', () => {
|
||||
mockApi([])
|
||||
|
||||
expect(
|
||||
await bitbucketConnector.validateConfig(ACCESS_TOKEN, { ...CONFIG, maxItems: '0' })
|
||||
).toEqual({ valid: false, error: 'Max items must be a positive number' })
|
||||
await bitbucketConnector.validateConfig(ACCESS_TOKEN, {
|
||||
...CONFIG,
|
||||
maxItems: '0',
|
||||
})
|
||||
).toEqual({ valid: false, error: 'Max items must be a positive integer' })
|
||||
})
|
||||
|
||||
it('rejects a fractional maxItems value', async () => {
|
||||
mockApi([])
|
||||
|
||||
expect(
|
||||
await bitbucketConnector.validateConfig(ACCESS_TOKEN, {
|
||||
...CONFIG,
|
||||
maxItems: '1.5',
|
||||
})
|
||||
).toEqual({ valid: false, error: 'Max items must be a positive integer' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
} from '@/connectors/utils'
|
||||
import {
|
||||
BITBUCKET_API_BASE,
|
||||
bitbucketApiUrl,
|
||||
bitbucketRepositoryPath,
|
||||
encodeBitbucketRepositoryPath,
|
||||
encodeBitbucketSegment,
|
||||
validateBitbucketOpaqueUrl,
|
||||
} from '@/tools/bitbucket/utils'
|
||||
|
||||
const logger = createLogger('BitbucketConnector')
|
||||
@@ -49,10 +50,10 @@ const PULL_REQUEST_PAGE_SIZE = 50
|
||||
* enumerates `reviewers`/`participants` as such. The pull request body is exactly
|
||||
* the kind of field that carve-out covers and the reference never promises it on
|
||||
* the collection, so rather than assume, the connector asks for it explicitly:
|
||||
* `+` additive syntax keeps every default field and adds `values.summary` on top.
|
||||
* Without it a listing that omits the body would index title-only documents.
|
||||
* `+` additive syntax keeps every default field and adds the rendered description
|
||||
* on top. `summary` is retained only as a fallback.
|
||||
*/
|
||||
const PULL_REQUEST_LIST_FIELDS = '+values.summary'
|
||||
const PULL_REQUEST_LIST_FIELDS = '+values.rendered.description'
|
||||
/**
|
||||
* Sort key for the pull request collection.
|
||||
*
|
||||
@@ -78,11 +79,11 @@ const BINARY_SNIFF_BYTES = 8000
|
||||
*/
|
||||
const MAX_TREE_DEPTH = 5
|
||||
const BINARY_SKIP_REASON = 'Binary file was not indexed'
|
||||
const NON_UTF8_SKIP_REASON = 'Non-UTF-8 file was not indexed'
|
||||
/**
|
||||
* Bitbucket answers a raw read of an LFS-managed file with a 301 to Atlassian's
|
||||
* media services platform, which the connector does not follow — the redirect is
|
||||
* cross-origin, so the bearer token would be stripped and the request would fail
|
||||
* anyway. The file surfaces once as a skipped row instead of being dropped.
|
||||
* media services platform. The connector deliberately surfaces the file as
|
||||
* unsupported instead of following an unvalidated cross-origin location.
|
||||
*/
|
||||
const LFS_SKIP_REASON = 'Git LFS file was not indexed'
|
||||
|
||||
@@ -114,11 +115,35 @@ interface BitbucketTreeEntry {
|
||||
}
|
||||
|
||||
interface BitbucketPagedResponse<T> {
|
||||
values?: T[]
|
||||
values: T[]
|
||||
/** Opaque absolute URL for the next page; absent on the last page. */
|
||||
next?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Bitbucket's guaranteed pagination envelope before reconciliation can
|
||||
* interpret it as a complete page. Treating a malformed response as an empty list
|
||||
* would make the sync engine hard-delete documents that still exist upstream.
|
||||
*/
|
||||
function parsePagedResponse<T>(value: unknown, resource: string): BitbucketPagedResponse<T> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error(`Bitbucket ${resource} response must be an object`)
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>
|
||||
if (!Array.isArray(record.values)) {
|
||||
throw new Error(`Bitbucket ${resource} response must include a values array`)
|
||||
}
|
||||
if (record.next !== undefined && typeof record.next !== 'string') {
|
||||
throw new Error(`Bitbucket ${resource} response next cursor must be a URL`)
|
||||
}
|
||||
|
||||
return {
|
||||
values: record.values as T[],
|
||||
next: record.next as string | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
interface BitbucketRenderedText {
|
||||
raw?: string
|
||||
markup?: string
|
||||
@@ -198,6 +223,17 @@ function readSlug(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
/** Reads an optional positive, finite integer document cap. */
|
||||
function readMaxItems(value: unknown): number {
|
||||
if (value === undefined || value === null) return 0
|
||||
if (typeof value === 'string' && !value.trim()) return 0
|
||||
const parsed = Number(value)
|
||||
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
||||
throw new Error('Max items must be a positive integer')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the parsed content-type choice from sourceConfig (defaults to 'code').
|
||||
*/
|
||||
@@ -439,10 +475,9 @@ function fileToStub(
|
||||
/**
|
||||
* Builds a pull request document from either the collection or the `self` record.
|
||||
*
|
||||
* The body is read from `summary.raw` — the text as the author typed it — which the
|
||||
* listing requests explicitly via {@link PULL_REQUEST_LIST_FIELDS}, so no per-document
|
||||
* fetch is needed. `rendered.description.raw` is the same text under the endpoint's
|
||||
* rendered-markup element and stands in on the rare record that carries only that.
|
||||
* The body is read from `rendered.description.raw`, the documented rendered-markup
|
||||
* representation of the user-provided description. `summary.raw` is a fallback
|
||||
* when the rendered description is absent.
|
||||
*/
|
||||
function pullRequestToDocument(
|
||||
repository: ResolvedRepository,
|
||||
@@ -453,7 +488,7 @@ function pullRequestToDocument(
|
||||
|
||||
const title = pullRequest.title?.trim() || `Pull request #${id}`
|
||||
const description =
|
||||
pullRequest.summary?.raw?.trim() || pullRequest.rendered?.description?.raw?.trim() || ''
|
||||
pullRequest.rendered?.description?.raw?.trim() || pullRequest.summary?.raw?.trim() || ''
|
||||
const body = composeBody(title, description)
|
||||
if (!body.trim()) return null
|
||||
|
||||
@@ -637,12 +672,17 @@ async function fetchSourceListing(
|
||||
accessToken: string,
|
||||
shallowUrl?: string
|
||||
): Promise<{ response: Response; depth: number }> {
|
||||
const response = await fetchWithRetry(url, { method: 'GET', headers: authHeaders(accessToken) })
|
||||
const response = await fetchWithRetry(url, {
|
||||
method: 'GET',
|
||||
headers: authHeaders(accessToken),
|
||||
})
|
||||
if (response.status !== 555 || !shallowUrl) {
|
||||
return { response, depth: requestedDepth }
|
||||
}
|
||||
|
||||
logger.warn('Bitbucket source listing timed out; retrying at depth 1', { url })
|
||||
logger.warn('Bitbucket source listing timed out; retrying at depth 1', {
|
||||
url,
|
||||
})
|
||||
const shallow = await fetchWithRetry(shallowUrl, {
|
||||
method: 'GET',
|
||||
headers: authHeaders(accessToken),
|
||||
@@ -710,6 +750,17 @@ function pendingDirectories(syncContext: Record<string, unknown> | undefined): s
|
||||
return syncContext.pendingDirs as string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a provider-truncated listing as unsafe for deletion reconciliation, even
|
||||
* during a forced full sync. Configured max-item caps use only `listingCapped`;
|
||||
* transient provider omissions must additionally use the engine's absolute gate.
|
||||
*/
|
||||
function markListingIncomplete(syncContext: Record<string, unknown> | undefined): void {
|
||||
if (!syncContext) return
|
||||
syncContext.listingCapped = true
|
||||
syncContext.listingTruncated = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the optional maxItems cap to a page, tracking the running total in
|
||||
* syncContext and flagging `listingCapped` when the cap truncates the listing.
|
||||
@@ -774,7 +825,7 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
const phases = activePhases(choice)
|
||||
if (phases.length === 0) return { documents: [], hasMore: false }
|
||||
|
||||
const maxItems = sourceConfig.maxItems ? Number(sourceConfig.maxItems) : 0
|
||||
const maxItems = readMaxItems(sourceConfig.maxItems)
|
||||
const repository = await resolveRepository(syncContext, workspaceSlug, repoSlug, accessToken)
|
||||
|
||||
let state = decodeCursor(cursor, phases[0])
|
||||
@@ -819,14 +870,28 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
/** Depth-1 retry target, offered only for URLs this connector built itself. */
|
||||
let shallowUrl: string | undefined
|
||||
if (state.nextUrl) {
|
||||
url = validateBitbucketOpaqueUrl(state.nextUrl)
|
||||
dir = state.dir ?? ''
|
||||
requestedDepth = state.depth ?? MAX_TREE_DEPTH
|
||||
const encodedDir = encodeBitbucketRepositoryPath(dir, true)
|
||||
const repositoryPath = bitbucketRepositoryPath(workspaceSlug, repoSlug)
|
||||
url = bitbucketApiUrl(
|
||||
`${repositoryPath}/src/${encodeBitbucketSegment(commit, 'commit')}/${encodedDir}`,
|
||||
{
|
||||
nextUrl: state.nextUrl,
|
||||
nextPathPrefix: `${repositoryPath}/src`,
|
||||
nextPathSuffix: encodedDir,
|
||||
nextRevision: commit,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const nextDir = frontier.shift()
|
||||
if (nextDir === undefined) {
|
||||
const adv = advance('code')
|
||||
return { documents: [], nextCursor: adv.nextCursor, hasMore: adv.hasMore }
|
||||
return {
|
||||
documents: [],
|
||||
nextCursor: adv.nextCursor,
|
||||
hasMore: adv.hasMore,
|
||||
}
|
||||
}
|
||||
dir = nextDir
|
||||
requestedDepth = MAX_TREE_DEPTH
|
||||
@@ -860,31 +925,36 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
repository: repository.fullName,
|
||||
dir,
|
||||
})
|
||||
if (syncContext) syncContext.listingCapped = true
|
||||
markListingIncomplete(syncContext)
|
||||
const skipped = continueCode(frontier)
|
||||
return { documents: [], nextCursor: skipped.nextCursor, hasMore: skipped.hasMore }
|
||||
return {
|
||||
documents: [],
|
||||
nextCursor: skipped.nextCursor,
|
||||
hasMore: skipped.hasMore,
|
||||
}
|
||||
}
|
||||
if (response.status === 404) {
|
||||
/**
|
||||
* `resolveRepository` already proved the repository is readable, so a 404
|
||||
* here can only mean the directory (or an empty repository's root) is
|
||||
* absent. Reconciliation is left enabled so its files delete normally.
|
||||
* Every directory was discovered at the same pinned commit, so a later 404
|
||||
* is an inconsistent partial listing rather than evidence that documents
|
||||
* were deleted. Keep walking, but block deletion reconciliation.
|
||||
*/
|
||||
logger.warn('Bitbucket source path not found; skipping', {
|
||||
repository: repository.fullName,
|
||||
dir,
|
||||
})
|
||||
markListingIncomplete(syncContext)
|
||||
const skipped = continueCode(frontier)
|
||||
return { documents: [], nextCursor: skipped.nextCursor, hasMore: skipped.hasMore }
|
||||
return {
|
||||
documents: [],
|
||||
nextCursor: skipped.nextCursor,
|
||||
hasMore: skipped.hasMore,
|
||||
}
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
/**
|
||||
* The token stopped working mid-sync. Flag the listing as incomplete so
|
||||
* deletion reconciliation does not hard-delete previously synced files.
|
||||
*/
|
||||
if (syncContext) syncContext.listingCapped = true
|
||||
const adv = advance('code')
|
||||
return { documents: [], nextCursor: adv.nextCursor, hasMore: adv.hasMore }
|
||||
throw new Error(
|
||||
`Bitbucket authorization failed while listing repository files: ${response.status}`
|
||||
)
|
||||
}
|
||||
const errorText = await response.text().catch(() => '')
|
||||
logger.error('Failed to list Bitbucket repository files', {
|
||||
@@ -894,14 +964,29 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
throw new Error(`Failed to list Bitbucket repository files: ${response.status}`)
|
||||
}
|
||||
|
||||
const page = (await response.json()) as BitbucketPagedResponse<BitbucketTreeEntry>
|
||||
const entries = Array.isArray(page.values) ? page.values : []
|
||||
const page = parsePagedResponse<BitbucketTreeEntry>(await response.json(), 'source listing')
|
||||
const entries = page.values
|
||||
const dirDepth = pathDepth(dir)
|
||||
const documents: ExternalDocument[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
||||
logger.warn('Skipping malformed Bitbucket source entry', {
|
||||
repository: repository.fullName,
|
||||
dir,
|
||||
})
|
||||
markListingIncomplete(syncContext)
|
||||
continue
|
||||
}
|
||||
const path = typeof entry.path === 'string' ? entry.path : ''
|
||||
if (!path) continue
|
||||
if (!path) {
|
||||
logger.warn('Skipping Bitbucket source entry without a path', {
|
||||
repository: repository.fullName,
|
||||
dir,
|
||||
})
|
||||
markListingIncomplete(syncContext)
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.type === 'commit_directory') {
|
||||
/**
|
||||
@@ -916,7 +1001,15 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.type !== 'commit_file') continue
|
||||
if (entry.type !== 'commit_file') {
|
||||
logger.warn('Skipping Bitbucket source entry with an unknown type', {
|
||||
repository: repository.fullName,
|
||||
dir,
|
||||
type: entry.type,
|
||||
})
|
||||
markListingIncomplete(syncContext)
|
||||
continue
|
||||
}
|
||||
|
||||
const attributes = entryAttributes(entry)
|
||||
if (attributes.has('binary') || attributes.has('link') || attributes.has('subrepository')) {
|
||||
@@ -945,17 +1038,28 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
if (page.next) {
|
||||
return {
|
||||
documents: capped,
|
||||
nextCursor: encodeCursor({ phase: 'code', nextUrl: page.next, dir, depth }),
|
||||
nextCursor: encodeCursor({
|
||||
phase: 'code',
|
||||
nextUrl: page.next,
|
||||
dir,
|
||||
depth,
|
||||
}),
|
||||
hasMore: true,
|
||||
}
|
||||
}
|
||||
const cont = continueCode(frontier)
|
||||
return { documents: capped, nextCursor: cont.nextCursor, hasMore: cont.hasMore }
|
||||
return {
|
||||
documents: capped,
|
||||
nextCursor: cont.nextCursor,
|
||||
hasMore: cont.hasMore,
|
||||
}
|
||||
}
|
||||
|
||||
let url: string
|
||||
if (state.nextUrl) {
|
||||
url = validateBitbucketOpaqueUrl(state.nextUrl)
|
||||
url = bitbucketApiUrl(`${bitbucketRepositoryPath(workspaceSlug, repoSlug)}/pullrequests`, {
|
||||
nextUrl: state.nextUrl,
|
||||
})
|
||||
} else {
|
||||
const prUrl = new URL(repositoryUrl(workspaceSlug, repoSlug, '/pullrequests'))
|
||||
prUrl.searchParams.set('pagelen', String(PULL_REQUEST_PAGE_SIZE))
|
||||
@@ -968,8 +1072,8 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
/**
|
||||
* The filtering reference is explicit that the paginated envelope's
|
||||
* `values.` prefix must not appear in a query field, so the field is
|
||||
* named bare even though the partial response above addresses it as
|
||||
* `values.summary`.
|
||||
* named bare even though the partial response above addresses nested
|
||||
* fields under `values`.
|
||||
*/
|
||||
prUrl.searchParams.set('q', `updated_on > ${bbqlDateTime(lastSyncAt)}`)
|
||||
}
|
||||
@@ -996,11 +1100,21 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
throw new Error(`Failed to list Bitbucket pull requests: ${response.status}`)
|
||||
}
|
||||
|
||||
const page = (await response.json()) as BitbucketPagedResponse<BitbucketPullRequest>
|
||||
const page = parsePagedResponse<BitbucketPullRequest>(
|
||||
await response.json(),
|
||||
'pull request listing'
|
||||
)
|
||||
const documents: ExternalDocument[] = []
|
||||
for (const pullRequest of Array.isArray(page.values) ? page.values : []) {
|
||||
for (const pullRequest of page.values) {
|
||||
const doc = pullRequestToDocument(repository, pullRequest)
|
||||
if (doc) documents.push(doc)
|
||||
if (doc) {
|
||||
documents.push(doc)
|
||||
} else {
|
||||
logger.warn('Skipping malformed Bitbucket pull request', {
|
||||
repository: repository.fullName,
|
||||
})
|
||||
markListingIncomplete(syncContext)
|
||||
}
|
||||
}
|
||||
|
||||
const { documents: capped, capped: hitLimit } = applyMaxItemsCap(
|
||||
@@ -1047,7 +1161,14 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
if (response.status === 404) return null
|
||||
throw new Error(`Failed to fetch Bitbucket pull request: ${response.status}`)
|
||||
}
|
||||
return pullRequestToDocument(repository, (await response.json()) as BitbucketPullRequest)
|
||||
const document = pullRequestToDocument(
|
||||
repository,
|
||||
(await response.json()) as BitbucketPullRequest
|
||||
)
|
||||
if (!document) {
|
||||
throw new Error(`Bitbucket pull request ${id} response was malformed`)
|
||||
}
|
||||
return document
|
||||
}
|
||||
|
||||
if (!externalId.startsWith(FILE_PREFIX)) return null
|
||||
@@ -1078,7 +1199,7 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
|
||||
const stub = fileToStub(repository, ref, commit, path, undefined)
|
||||
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
if (response.status === 301) {
|
||||
logger.info('Skipping Bitbucket LFS-managed file', { path })
|
||||
return markSkipped(stub, LFS_SKIP_REASON)
|
||||
}
|
||||
@@ -1088,7 +1209,10 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
|
||||
const buffer = await readBodyWithLimit(response, MAX_FILE_SIZE)
|
||||
if (buffer === null) {
|
||||
logger.info('Skipping oversized Bitbucket file', { path, limit: MAX_FILE_SIZE })
|
||||
logger.info('Skipping oversized Bitbucket file', {
|
||||
path,
|
||||
limit: MAX_FILE_SIZE,
|
||||
})
|
||||
return markSkipped(stub, sizeLimitSkipReason(MAX_FILE_SIZE))
|
||||
}
|
||||
if (isBinaryBuffer(buffer)) {
|
||||
@@ -1096,7 +1220,15 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
return markSkipped(stub, BINARY_SKIP_REASON)
|
||||
}
|
||||
|
||||
const body = composeBody(stub.title, buffer.toString('utf8'))
|
||||
let text: string
|
||||
try {
|
||||
text = new TextDecoder('utf-8', { fatal: true }).decode(buffer)
|
||||
} catch {
|
||||
logger.info('Skipping non-UTF-8 Bitbucket file', { path })
|
||||
return markSkipped(stub, NON_UTF8_SKIP_REASON)
|
||||
}
|
||||
|
||||
const body = composeBody(stub.title, text)
|
||||
if (!body.trim()) return null
|
||||
|
||||
return {
|
||||
@@ -1130,9 +1262,13 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
const repoSlug = readSlug(sourceConfig.repoSlug)
|
||||
if (!repoSlug) return { valid: false, error: 'Repository is required' }
|
||||
|
||||
const maxItems = sourceConfig.maxItems as string | undefined
|
||||
if (maxItems && (Number.isNaN(Number(maxItems)) || Number(maxItems) <= 0)) {
|
||||
return { valid: false, error: 'Max items must be a positive number' }
|
||||
try {
|
||||
readMaxItems(sourceConfig.maxItems)
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: getErrorMessage(error, 'Max items must be a positive integer'),
|
||||
}
|
||||
}
|
||||
|
||||
const choice = getContentTypeChoice(sourceConfig)
|
||||
@@ -1152,10 +1288,16 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
}
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { valid: false, error: 'Invalid credential or insufficient permissions' }
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid credential or insufficient permissions',
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
return { valid: false, error: `Cannot access repository: ${response.status}` }
|
||||
return {
|
||||
valid: false,
|
||||
error: `Cannot access repository: ${response.status}`,
|
||||
}
|
||||
}
|
||||
|
||||
const record = (await response.json()) as BitbucketRepositoryRecord
|
||||
@@ -1187,7 +1329,10 @@ export const bitbucketConnector: ConnectorConfig = {
|
||||
|
||||
return { valid: true }
|
||||
} catch (error) {
|
||||
return { valid: false, error: getErrorMessage(error, 'Failed to validate configuration') }
|
||||
return {
|
||||
valid: false,
|
||||
error: getErrorMessage(error, 'Failed to validate configuration'),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const bitbucketConnectorMeta: ConnectorMeta = {
|
||||
auth: {
|
||||
mode: 'oauth',
|
||||
provider: 'bitbucket',
|
||||
requiredScopes: ['repository', 'pullrequest'],
|
||||
requiredScopes: ['pullrequest'],
|
||||
},
|
||||
|
||||
configFields: [
|
||||
@@ -88,7 +88,7 @@ export const bitbucketConnectorMeta: ConnectorMeta = {
|
||||
],
|
||||
placeholder: 'Code (repository files) only',
|
||||
description:
|
||||
'Which content to index. Defaults to repository files when left unset. Bitbucket Cloud wikis and the issue tracker are not indexed — see the connector notes.',
|
||||
'Which content to index. Defaults to repository files when left unset. Git LFS-managed files, Bitbucket Cloud wikis, and the issue tracker are not indexed.',
|
||||
},
|
||||
{
|
||||
id: 'ref',
|
||||
|
||||
@@ -128,6 +128,7 @@ describe('Bitbucket OAuth Connector', () => {
|
||||
'pullrequest:write',
|
||||
'pipeline',
|
||||
'pipeline:write',
|
||||
'webhook',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: false,
|
||||
@@ -150,6 +151,7 @@ describe('Bitbucket OAuth Connector', () => {
|
||||
'pullrequest:write',
|
||||
'pipeline',
|
||||
'pipeline:write',
|
||||
'webhook',
|
||||
]
|
||||
const mockFetch = createMockFetch({
|
||||
json: {
|
||||
@@ -188,6 +190,29 @@ describe('Bitbucket OAuth Connector', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes Bitbucket’s singular scope response field', async () => {
|
||||
const getToken = getBitbucketConnector().getToken
|
||||
if (!getToken) throw new Error('Bitbucket connector must define getToken')
|
||||
|
||||
const tokens = await withMockFetch(
|
||||
createMockFetch({
|
||||
json: {
|
||||
access_token: 'bitbucket_access_token',
|
||||
refresh_token: 'bitbucket_refresh_token',
|
||||
scope: 'account repository pullrequest webhook',
|
||||
token_type: 'bearer',
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
getToken({
|
||||
code: 'authorization_code',
|
||||
redirectURI: 'http://localhost:3000/api/auth/oauth2/callback/bitbucket',
|
||||
})
|
||||
)
|
||||
|
||||
expect(tokens.scopes).toEqual(['account', 'repository', 'pullrequest', 'webhook'])
|
||||
})
|
||||
|
||||
it('uses account_id before uuid and always synthesizes an internal email', async () => {
|
||||
const connector = getBitbucketConnector()
|
||||
const getUserInfo = connector.getUserInfo
|
||||
|
||||
@@ -731,6 +731,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'pullrequest:write',
|
||||
'pipeline',
|
||||
'pipeline:write',
|
||||
'webhook',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -391,6 +391,7 @@ describe('getScopeDescription', () => {
|
||||
'View your Bitbucket account and workspace memberships'
|
||||
)
|
||||
expect(getScopeDescription('pipeline:write', 'bitbucket')).toBe('Run and stop pipelines')
|
||||
expect(getScopeDescription('webhook', 'bitbucket')).toBe('Manage repository webhooks')
|
||||
})
|
||||
|
||||
it.concurrent('preserves the existing Reddit meaning of the account scope', () => {
|
||||
@@ -664,6 +665,19 @@ describe('getScopesForService', () => {
|
||||
expect(scopes).not.toContain('Calendars.ReadWrite.Shared')
|
||||
})
|
||||
|
||||
it.concurrent('should include webhook management in Bitbucket consent scopes', () => {
|
||||
expect(getScopesForService('bitbucket')).toEqual([
|
||||
'account',
|
||||
'repository',
|
||||
'repository:write',
|
||||
'pullrequest',
|
||||
'pullrequest:write',
|
||||
'pipeline',
|
||||
'pipeline:write',
|
||||
'webhook',
|
||||
])
|
||||
})
|
||||
|
||||
it.concurrent('should return empty array for empty string', () => {
|
||||
const scopes = getScopesForService('')
|
||||
|
||||
|
||||
@@ -484,6 +484,7 @@ const PROVIDER_SCOPE_DESCRIPTIONS: Readonly<Record<string, Readonly<Record<strin
|
||||
'pullrequest:write': 'Create, update, approve, decline, and merge pull requests',
|
||||
pipeline: 'View pipelines, steps, and logs',
|
||||
'pipeline:write': 'Run and stop pipelines',
|
||||
webhook: 'Manage repository webhooks',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,666 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { getErrorMessage, toError } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toRecord, toRecordOrNull } from '@sim/utils/object'
|
||||
import { truncate } from '@sim/utils/string'
|
||||
import { refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service'
|
||||
import {
|
||||
getCredentialOwner,
|
||||
getNotificationUrl,
|
||||
getProviderConfig,
|
||||
} from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
DeleteSubscriptionContext,
|
||||
EventMatchContext,
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
SubscriptionContext,
|
||||
SubscriptionResult,
|
||||
WebhookProviderHandler,
|
||||
} from '@/lib/webhooks/providers/types'
|
||||
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
|
||||
import {
|
||||
BITBUCKET_API_BASE,
|
||||
bitbucketHeaders,
|
||||
bitbucketRepositoryPath,
|
||||
encodeBitbucketSegment,
|
||||
} from '@/tools/bitbucket/utils'
|
||||
|
||||
const logger = createLogger('WebhookProvider:Bitbucket')
|
||||
const BITBUCKET_MANAGEMENT_REQUEST_TIMEOUT_MS = 15_000
|
||||
|
||||
const PULL_REQUEST_TRIGGER_IDS = new Set([
|
||||
'bitbucket_pull_request_created',
|
||||
'bitbucket_pull_request_updated',
|
||||
'bitbucket_pull_request_approved',
|
||||
'bitbucket_pull_request_approval_removed',
|
||||
'bitbucket_pull_request_changes_requested',
|
||||
'bitbucket_pull_request_changes_request_removed',
|
||||
'bitbucket_pull_request_merged',
|
||||
'bitbucket_pull_request_declined',
|
||||
'bitbucket_pull_request_comment_created',
|
||||
'bitbucket_pull_request_comment_updated',
|
||||
'bitbucket_pull_request_comment_deleted',
|
||||
'bitbucket_pull_request_comment_resolved',
|
||||
'bitbucket_pull_request_comment_reopened',
|
||||
])
|
||||
|
||||
const PULL_REQUEST_APPROVAL_TRIGGER_IDS = new Set([
|
||||
'bitbucket_pull_request_approved',
|
||||
'bitbucket_pull_request_approval_removed',
|
||||
])
|
||||
|
||||
const PULL_REQUEST_CHANGES_REQUEST_TRIGGER_IDS = new Set([
|
||||
'bitbucket_pull_request_changes_requested',
|
||||
'bitbucket_pull_request_changes_request_removed',
|
||||
])
|
||||
|
||||
const PULL_REQUEST_COMMENT_TRIGGER_IDS = new Set([
|
||||
'bitbucket_pull_request_comment_created',
|
||||
'bitbucket_pull_request_comment_updated',
|
||||
'bitbucket_pull_request_comment_deleted',
|
||||
'bitbucket_pull_request_comment_resolved',
|
||||
'bitbucket_pull_request_comment_reopened',
|
||||
])
|
||||
|
||||
function bitbucketHooksUrl(workspaceSlug: string, repoSlug: string): string {
|
||||
return `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(workspaceSlug, repoSlug)}/hooks`
|
||||
}
|
||||
|
||||
function fetchBitbucketManagement(url: string, init: RequestInit = {}): Promise<Response> {
|
||||
return fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(BITBUCKET_MANAGEMENT_REQUEST_TIMEOUT_MS),
|
||||
})
|
||||
}
|
||||
|
||||
function validateBitbucketSignature(secret: string, signature: string, body: string): boolean {
|
||||
const match = /^sha256=([a-f\d]{64})$/i.exec(signature.trim())
|
||||
if (!secret || !match) return false
|
||||
|
||||
const expected = hmacSha256Hex(body, secret)
|
||||
return safeCompare(expected, match[1].toLowerCase())
|
||||
}
|
||||
|
||||
async function resolveBitbucketAccessToken(
|
||||
credentialId: string | undefined,
|
||||
requestId: string
|
||||
): Promise<string> {
|
||||
if (!credentialId) {
|
||||
throw new Error(
|
||||
'Bitbucket account connection required. Connect or reconnect Bitbucket in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const owner = await getCredentialOwner(credentialId, requestId)
|
||||
const accessToken = owner
|
||||
? await refreshAccessTokenIfNeeded(owner.accountId, owner.userId, requestId)
|
||||
: null
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
'Bitbucket account connection required. Connect or reconnect Bitbucket in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
return accessToken
|
||||
}
|
||||
|
||||
function readRequiredConfigString(
|
||||
config: Record<string, unknown>,
|
||||
key: 'workspaceSlug' | 'repoSlug',
|
||||
label: string
|
||||
): string {
|
||||
const value = config[key]
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new Error(`Bitbucket ${label} is required to manage the repository webhook.`)
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
async function readBitbucketErrorDetail(response: Response): Promise<string> {
|
||||
const payload = toRecord(await response.json().catch(() => null))
|
||||
const error = toRecord(payload.error)
|
||||
const message = error.message
|
||||
return typeof message === 'string' && message.trim() ? truncate(message.trim(), 300) : ''
|
||||
}
|
||||
|
||||
function createBitbucketApiError(status: number, action: 'create' | 'delete', detail = ''): Error {
|
||||
const operation = action === 'create' ? 'create' : 'delete'
|
||||
if (status === 401) {
|
||||
return new Error(
|
||||
`Bitbucket authentication failed while trying to ${operation} the webhook. Reconnect the Bitbucket account and try again.`
|
||||
)
|
||||
}
|
||||
if (status === 403) {
|
||||
return new Error(
|
||||
`Bitbucket denied permission to ${operation} the webhook. The connected account must be a repository administrator and grant the webhook scope.`
|
||||
)
|
||||
}
|
||||
if (status === 404) {
|
||||
return new Error(
|
||||
`Bitbucket repository not found while trying to ${operation} the webhook. Verify the workspace and repository selections.`
|
||||
)
|
||||
}
|
||||
if (status === 429) {
|
||||
return new Error(`Bitbucket rate limited the webhook ${operation} request. Try again shortly.`)
|
||||
}
|
||||
if (status === 400 && action === 'create') {
|
||||
return new Error(
|
||||
detail
|
||||
? `Bitbucket rejected the webhook creation request: ${detail}`
|
||||
: 'Bitbucket rejected the webhook creation request. Check the repository webhook limit and trigger configuration.'
|
||||
)
|
||||
}
|
||||
return new Error(
|
||||
`Failed to ${operation} Bitbucket webhook: ${status}${detail ? ` (${detail})` : ''}`
|
||||
)
|
||||
}
|
||||
|
||||
type BitbucketCandidateLookup =
|
||||
| { kind: 'absent' }
|
||||
| {
|
||||
kind: 'found'
|
||||
externalId: string
|
||||
active: boolean
|
||||
events: string[]
|
||||
secretSet: boolean
|
||||
}
|
||||
| { kind: 'ambiguous'; matchCount: number }
|
||||
| { kind: 'unavailable'; error: Error }
|
||||
|
||||
function bitbucketHookDescription(triggerId: string, webhookId: string): string {
|
||||
return `Sim workflow trigger (${triggerId}) [sim:${webhookId}]`
|
||||
}
|
||||
|
||||
async function findBitbucketCandidateHook(
|
||||
workspaceSlug: string,
|
||||
repoSlug: string,
|
||||
accessToken: string,
|
||||
callbackUrl: string,
|
||||
description: string,
|
||||
requestId: string
|
||||
): Promise<BitbucketCandidateLookup> {
|
||||
const hooksUrl = bitbucketHooksUrl(workspaceSlug, repoSlug)
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetchBitbucketManagement(`${hooksUrl}?pagelen=100`, {
|
||||
headers: bitbucketHeaders(accessToken),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Could not inspect Bitbucket hooks for candidate reconciliation`, {
|
||||
error: getErrorMessage(error),
|
||||
})
|
||||
return {
|
||||
kind: 'unavailable',
|
||||
error: toError(error),
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await readBitbucketErrorDetail(response)
|
||||
return {
|
||||
kind: 'unavailable',
|
||||
error: createBitbucketApiError(response.status, 'create', detail),
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = await response.json().catch(() => null)
|
||||
const payload = toRecord(parsed)
|
||||
if (!Array.isArray(payload.values)) {
|
||||
return {
|
||||
kind: 'unavailable',
|
||||
error: new Error('Bitbucket returned a malformed webhook-list response.'),
|
||||
}
|
||||
}
|
||||
|
||||
const matchingHooks = payload.values.filter((value) => {
|
||||
const hook = toRecord(value)
|
||||
return hook.url === callbackUrl && hook.description === description
|
||||
})
|
||||
|
||||
if (matchingHooks.length === 0) return { kind: 'absent' }
|
||||
if (matchingHooks.length !== 1) {
|
||||
return { kind: 'ambiguous', matchCount: matchingHooks.length }
|
||||
}
|
||||
|
||||
const matchedHook = toRecord(matchingHooks[0])
|
||||
const externalId = nullableString(matchedHook.uuid)?.trim()
|
||||
return externalId
|
||||
? {
|
||||
kind: 'found',
|
||||
externalId,
|
||||
active: matchedHook.active === true,
|
||||
events: Array.isArray(matchedHook.events)
|
||||
? matchedHook.events.filter((event): event is string => typeof event === 'string')
|
||||
: [],
|
||||
secretSet: matchedHook.secret_set === true,
|
||||
}
|
||||
: { kind: 'ambiguous', matchCount: matchingHooks.length }
|
||||
}
|
||||
|
||||
async function deleteProvenBitbucketCandidate(
|
||||
workspaceSlug: string,
|
||||
repoSlug: string,
|
||||
accessToken: string,
|
||||
externalId: string,
|
||||
requestId: string
|
||||
): Promise<boolean> {
|
||||
const hooksUrl = bitbucketHooksUrl(workspaceSlug, repoSlug)
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetchBitbucketManagement(
|
||||
`${hooksUrl}/${encodeBitbucketSegment(externalId, 'externalId')}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: bitbucketHeaders(accessToken),
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Failed to delete proven Bitbucket candidate ${externalId}`, {
|
||||
error: getErrorMessage(error),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to delete proven Bitbucket candidate ${externalId} (${response.status})`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function rollbackAmbiguousBitbucketCandidate(
|
||||
workspaceSlug: string,
|
||||
repoSlug: string,
|
||||
accessToken: string,
|
||||
callbackUrl: string,
|
||||
description: string,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
const candidate = await findBitbucketCandidateHook(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
callbackUrl,
|
||||
description,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (candidate.kind !== 'found') {
|
||||
logger.warn(
|
||||
`[${requestId}] Bitbucket candidate cleanup skipped because identity was not unique`,
|
||||
{
|
||||
lookup: candidate.kind,
|
||||
matchCount: candidate.kind === 'ambiguous' ? candidate.matchCount : undefined,
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await deleteProvenBitbucketCandidate(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
candidate.externalId,
|
||||
requestId
|
||||
)
|
||||
}
|
||||
|
||||
function nullableString(value: unknown): string | null {
|
||||
return typeof value === 'string' ? value : null
|
||||
}
|
||||
|
||||
function nullableNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
}
|
||||
|
||||
function headerValue(headers: Record<string, string>, name: string): string | null {
|
||||
const value = headers[name.toLowerCase()]
|
||||
return typeof value === 'string' && value ? value : null
|
||||
}
|
||||
|
||||
function attemptNumber(headers: Record<string, string>): number | null {
|
||||
const raw = headerValue(headers, 'x-attempt-number')
|
||||
if (!raw) return null
|
||||
const parsed = Number(raw)
|
||||
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null
|
||||
}
|
||||
|
||||
function commentFields(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const comment = toRecordOrNull(body.comment)
|
||||
const content = toRecord(comment?.content)
|
||||
return {
|
||||
comment,
|
||||
commentId: nullableNumber(comment?.id),
|
||||
commentContent: nullableString(content.raw),
|
||||
}
|
||||
}
|
||||
|
||||
function pullRequestFields(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const pullRequest = toRecordOrNull(body.pullrequest)
|
||||
const source = toRecord(pullRequest?.source)
|
||||
const destination = toRecord(pullRequest?.destination)
|
||||
const sourceBranch = toRecord(source.branch)
|
||||
const destinationBranch = toRecord(destination.branch)
|
||||
|
||||
return {
|
||||
pullRequest,
|
||||
pullRequestId: nullableNumber(pullRequest?.id),
|
||||
pullRequestTitle: nullableString(pullRequest?.title),
|
||||
pullRequestState: nullableString(pullRequest?.state),
|
||||
sourceBranch: nullableString(sourceBranch.name),
|
||||
destinationBranch: nullableString(destinationBranch.name),
|
||||
}
|
||||
}
|
||||
|
||||
function commitHashFromStatus(commitStatus: Record<string, unknown> | null): string | null {
|
||||
const links = toRecord(commitStatus?.links)
|
||||
const commit = toRecord(links.commit)
|
||||
const href = nullableString(commit.href)
|
||||
if (!href) return null
|
||||
|
||||
try {
|
||||
const pathSegments = new URL(href).pathname.split('/').filter(Boolean)
|
||||
const encodedHash = pathSegments.at(-1)
|
||||
return encodedHash ? decodeURIComponent(encodedHash) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatBitbucketInput(
|
||||
triggerId: string | undefined,
|
||||
body: unknown,
|
||||
headers: Record<string, string>
|
||||
): Record<string, unknown> {
|
||||
const b = toRecord(body)
|
||||
const base: Record<string, unknown> = {
|
||||
eventType: headerValue(headers, 'x-event-key'),
|
||||
hookUuid: headerValue(headers, 'x-hook-uuid'),
|
||||
requestUuid: headerValue(headers, 'x-request-uuid'),
|
||||
attemptNumber: attemptNumber(headers),
|
||||
actor: toRecordOrNull(b.actor),
|
||||
repository: toRecordOrNull(b.repository),
|
||||
payload: body ?? null,
|
||||
}
|
||||
|
||||
if (triggerId === 'bitbucket_push') return { ...base, push: toRecordOrNull(b.push) }
|
||||
if (triggerId === 'bitbucket_repository_forked') {
|
||||
return { ...base, fork: toRecordOrNull(b.fork) }
|
||||
}
|
||||
if (triggerId === 'bitbucket_repository_updated') {
|
||||
return { ...base, changes: toRecordOrNull(b.changes) }
|
||||
}
|
||||
if (triggerId === 'bitbucket_commit_comment_created') {
|
||||
return { ...base, ...commentFields(b), commit: toRecordOrNull(b.commit) }
|
||||
}
|
||||
if (
|
||||
triggerId === 'bitbucket_build_status_created' ||
|
||||
triggerId === 'bitbucket_build_status_updated'
|
||||
) {
|
||||
const commitStatus = toRecordOrNull(b.commit_status)
|
||||
return {
|
||||
...base,
|
||||
commitStatus,
|
||||
commitHash: commitHashFromStatus(commitStatus),
|
||||
statusKey: nullableString(commitStatus?.key),
|
||||
statusState: nullableString(commitStatus?.state),
|
||||
statusName: nullableString(commitStatus?.name),
|
||||
statusUrl: nullableString(commitStatus?.url),
|
||||
}
|
||||
}
|
||||
if (triggerId && PULL_REQUEST_TRIGGER_IDS.has(triggerId)) {
|
||||
const input = { ...base, ...pullRequestFields(b) }
|
||||
if (PULL_REQUEST_APPROVAL_TRIGGER_IDS.has(triggerId)) {
|
||||
return { ...input, approval: toRecordOrNull(b.approval) }
|
||||
}
|
||||
if (PULL_REQUEST_CHANGES_REQUEST_TRIGGER_IDS.has(triggerId)) {
|
||||
return { ...input, changesRequest: toRecordOrNull(b.changes_request) }
|
||||
}
|
||||
if (PULL_REQUEST_COMMENT_TRIGGER_IDS.has(triggerId)) {
|
||||
return { ...input, ...commentFields(b) }
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
export const bitbucketHandler: WebhookProviderHandler = {
|
||||
/**
|
||||
* Bitbucket supplies a request UUID alongside its attempt number. Preserve that provider-owned
|
||||
* identifier for deduplication instead of fingerprinting payloads, since two legitimate events
|
||||
* can have identical bodies. The processor strips untrusted Sim idempotency headers before this
|
||||
* hook runs.
|
||||
*/
|
||||
enrichHeaders(_ctx, headers) {
|
||||
const requestUuid = headers['x-request-uuid']?.trim()
|
||||
if (requestUuid) headers['x-sim-idempotency-key'] = requestUuid
|
||||
},
|
||||
|
||||
verifyAuth: createHmacVerifier({
|
||||
configKey: 'webhookSecret',
|
||||
headerName: 'X-Hub-Signature',
|
||||
validateFn: validateBitbucketSignature,
|
||||
providerLabel: 'Bitbucket',
|
||||
requireSecret: true,
|
||||
}),
|
||||
|
||||
async matchEvent({ request, requestId, providerConfig }: EventMatchContext) {
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
const eventKey = request.headers.get('X-Event-Key') || ''
|
||||
const { isBitbucketEventMatch } = await import('@/triggers/bitbucket/utils')
|
||||
|
||||
if (!triggerId || !isBitbucketEventMatch(triggerId, eventKey)) {
|
||||
logger.debug(
|
||||
`[${requestId}] Bitbucket event '${eventKey}' does not match trigger '${triggerId || 'unknown'}', skipping`
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
async formatInput({ body, headers, webhook }: FormatInputContext): Promise<FormatInputResult> {
|
||||
const config = getProviderConfig(webhook)
|
||||
return {
|
||||
input: formatBitbucketInput(config.triggerId as string | undefined, body, headers),
|
||||
}
|
||||
},
|
||||
|
||||
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
|
||||
const config = getProviderConfig(ctx.webhook)
|
||||
const triggerId = config.triggerId as string | undefined
|
||||
const credentialId = config.credentialId as string | undefined
|
||||
const workspaceSlug = readRequiredConfigString(config, 'workspaceSlug', 'workspace')
|
||||
const repoSlug = readRequiredConfigString(config, 'repoSlug', 'repository')
|
||||
const webhookId = nullableString(ctx.webhook.id)?.trim()
|
||||
if (!webhookId) {
|
||||
throw new Error('Bitbucket webhook ID is required to manage the repository webhook.')
|
||||
}
|
||||
|
||||
const { getBitbucketEventForTrigger } = await import('@/triggers/bitbucket/utils')
|
||||
const eventKey = triggerId ? getBitbucketEventForTrigger(triggerId) : undefined
|
||||
if (!triggerId || !eventKey) {
|
||||
throw new Error(`Unknown Bitbucket trigger type: ${triggerId || 'missing'}`)
|
||||
}
|
||||
|
||||
const accessToken = await resolveBitbucketAccessToken(credentialId, ctx.requestId)
|
||||
const callbackUrl = getNotificationUrl(ctx.webhook)
|
||||
const description = bitbucketHookDescription(triggerId, webhookId)
|
||||
const hooksUrl = bitbucketHooksUrl(workspaceSlug, repoSlug)
|
||||
const isStableCandidate = ctx.webhook.registrationStatus === 'candidate'
|
||||
|
||||
if (isStableCandidate) {
|
||||
const existingCandidate = await findBitbucketCandidateHook(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
callbackUrl,
|
||||
description,
|
||||
ctx.requestId
|
||||
)
|
||||
|
||||
if (existingCandidate.kind === 'unavailable') throw existingCandidate.error
|
||||
if (existingCandidate.kind === 'ambiguous') {
|
||||
throw new Error(
|
||||
`Bitbucket candidate reconciliation found ${existingCandidate.matchCount} matching hooks; no hook was deleted.`
|
||||
)
|
||||
}
|
||||
if (existingCandidate.kind === 'found') {
|
||||
const checkpointedExternalId = nullableString(config.externalId)?.trim()
|
||||
const checkpointedSecret = nullableString(config.webhookSecret)?.trim()
|
||||
const candidateMatchesCheckpoint =
|
||||
checkpointedExternalId === existingCandidate.externalId &&
|
||||
Boolean(checkpointedSecret) &&
|
||||
existingCandidate.active &&
|
||||
existingCandidate.secretSet &&
|
||||
existingCandidate.events.length === 1 &&
|
||||
existingCandidate.events[0] === eventKey
|
||||
if (candidateMatchesCheckpoint && checkpointedSecret) {
|
||||
logger.info(
|
||||
`[${ctx.requestId}] Reusing checkpointed Bitbucket candidate ${existingCandidate.externalId}`
|
||||
)
|
||||
return {
|
||||
providerConfigUpdates: {
|
||||
externalId: existingCandidate.externalId,
|
||||
webhookSecret: checkpointedSecret,
|
||||
eventTypes: [eventKey],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await deleteProvenBitbucketCandidate(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
existingCandidate.externalId,
|
||||
ctx.requestId
|
||||
)
|
||||
if (!deleted) {
|
||||
throw new Error(
|
||||
'Could not safely remove a stale Bitbucket candidate webhook; no new hook was created.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const webhookSecret = generateId()
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetchBitbucketManagement(hooksUrl, {
|
||||
method: 'POST',
|
||||
headers: bitbucketHeaders(accessToken, { json: true }),
|
||||
body: JSON.stringify({
|
||||
description,
|
||||
url: callbackUrl,
|
||||
active: true,
|
||||
secret: webhookSecret,
|
||||
events: [eventKey],
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
if (isStableCandidate) {
|
||||
await rollbackAmbiguousBitbucketCandidate(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
callbackUrl,
|
||||
description,
|
||||
ctx.requestId
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await readBitbucketErrorDetail(response)
|
||||
logger.error(`[${ctx.requestId}] Failed to create Bitbucket webhook (${response.status})`, {
|
||||
detail,
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
triggerId,
|
||||
})
|
||||
const apiError = createBitbucketApiError(response.status, 'create', detail)
|
||||
if (isStableCandidate && (response.status === 408 || response.status >= 500)) {
|
||||
await rollbackAmbiguousBitbucketCandidate(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
callbackUrl,
|
||||
description,
|
||||
ctx.requestId
|
||||
)
|
||||
}
|
||||
throw apiError
|
||||
}
|
||||
|
||||
const created = toRecord(await response.json().catch(() => null))
|
||||
const externalId = nullableString(created.uuid)?.trim() || null
|
||||
if (!externalId) {
|
||||
if (isStableCandidate) {
|
||||
await rollbackAmbiguousBitbucketCandidate(
|
||||
workspaceSlug,
|
||||
repoSlug,
|
||||
accessToken,
|
||||
callbackUrl,
|
||||
description,
|
||||
ctx.requestId
|
||||
)
|
||||
}
|
||||
throw new Error('Bitbucket webhook was created but no hook UUID was returned.')
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${ctx.requestId}] Created Bitbucket webhook ${externalId} for ${workspaceSlug}/${repoSlug}`
|
||||
)
|
||||
return {
|
||||
providerConfigUpdates: { externalId, webhookSecret, eventTypes: [eventKey] },
|
||||
}
|
||||
},
|
||||
|
||||
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
|
||||
try {
|
||||
const config = getProviderConfig(ctx.webhook)
|
||||
const credentialId = config.credentialId as string | undefined
|
||||
const externalId =
|
||||
typeof config.externalId === 'string' ? config.externalId.trim() : undefined
|
||||
const workspaceSlug =
|
||||
typeof config.workspaceSlug === 'string' ? config.workspaceSlug.trim() : undefined
|
||||
const repoSlug = typeof config.repoSlug === 'string' ? config.repoSlug.trim() : undefined
|
||||
|
||||
if (!credentialId || !externalId || !workspaceSlug || !repoSlug) {
|
||||
const message =
|
||||
'Missing Bitbucket credential, workspace, repository, or hook UUID for webhook deletion.'
|
||||
if (ctx.strict) throw new Error(message)
|
||||
logger.warn(`[${ctx.requestId}] ${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
const accessToken = await resolveBitbucketAccessToken(credentialId, ctx.requestId)
|
||||
const hooksUrl = bitbucketHooksUrl(workspaceSlug, repoSlug)
|
||||
const response = await fetchBitbucketManagement(
|
||||
`${hooksUrl}/${encodeBitbucketSegment(externalId, 'externalId')}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: bitbucketHeaders(accessToken),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
const detail = await readBitbucketErrorDetail(response)
|
||||
throw createBitbucketApiError(response.status, 'delete', detail)
|
||||
}
|
||||
|
||||
logger.info(`[${ctx.requestId}] Deleted Bitbucket webhook ${externalId}`)
|
||||
} catch (error) {
|
||||
logger.warn(`[${ctx.requestId}] Error deleting Bitbucket webhook (non-fatal)`, error)
|
||||
if (ctx.strict) throw error
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { airtableHandler } from '@/lib/webhooks/providers/airtable'
|
||||
import { ashbyHandler } from '@/lib/webhooks/providers/ashby'
|
||||
import { attioHandler } from '@/lib/webhooks/providers/attio'
|
||||
import { azureDevOpsHandler } from '@/lib/webhooks/providers/azure-devops'
|
||||
import { bitbucketHandler } from '@/lib/webhooks/providers/bitbucket'
|
||||
import { calcomHandler } from '@/lib/webhooks/providers/calcom'
|
||||
import { calendlyHandler } from '@/lib/webhooks/providers/calendly'
|
||||
import { circlebackHandler } from '@/lib/webhooks/providers/circleback'
|
||||
@@ -70,6 +71,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
|
||||
ashby: ashbyHandler,
|
||||
attio: attioHandler,
|
||||
azure_devops: azureDevOpsHandler,
|
||||
bitbucket: bitbucketHandler,
|
||||
calendly: calendlyHandler,
|
||||
calcom: calcomHandler,
|
||||
circleback: circlebackHandler,
|
||||
|
||||
@@ -7,6 +7,7 @@ vi.unmock('@/blocks/registry')
|
||||
|
||||
import * as blocksBarrel from '@/blocks'
|
||||
import { getAllBlocks, getBlock as getRealBlock } from '@/blocks/registry'
|
||||
import { bitbucketSelectors } from '@/hooks/selectors/providers/bitbucket/selectors'
|
||||
import {
|
||||
buildSelectorContextFromBlock,
|
||||
getSelectorContextSubBlocks,
|
||||
@@ -230,6 +231,36 @@ describe('buildSelectorContextFromBlock', () => {
|
||||
expect(ctx.oauthCredential).toBeUndefined()
|
||||
})
|
||||
|
||||
it('exposes a trigger workspace slug to the Bitbucket repository selector', () => {
|
||||
const context = buildSelectorContextFromBlock(
|
||||
'bitbucket',
|
||||
{
|
||||
selectedTriggerId: {
|
||||
id: 'selectedTriggerId',
|
||||
type: 'dropdown',
|
||||
value: 'bitbucket_push',
|
||||
},
|
||||
triggerCredentials: {
|
||||
id: 'triggerCredentials',
|
||||
type: 'oauth-input',
|
||||
value: 'credential-1',
|
||||
},
|
||||
workspacePicker: {
|
||||
id: 'workspacePicker',
|
||||
type: 'project-selector',
|
||||
value: 'acme-platform',
|
||||
},
|
||||
},
|
||||
{ triggerMode: true }
|
||||
)
|
||||
|
||||
expect(context).toMatchObject({
|
||||
oauthCredential: 'credential-1',
|
||||
workspaceSlug: 'acme-platform',
|
||||
})
|
||||
expect(bitbucketSelectors['bitbucket.repositories'].enabled({ context } as never)).toBe(true)
|
||||
})
|
||||
|
||||
it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => {
|
||||
const ctx = buildSelectorContextFromBlock('knowledge', {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketBuildStatusCreatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_build_status_created',
|
||||
name: 'Bitbucket Build Status Created',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a build status is created for a Bitbucket commit',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_build_status_created',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Build Status Created'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_build_status_created'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_build_status_created'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_build_status_created,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketBuildStatusUpdatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_build_status_updated',
|
||||
name: 'Bitbucket Build Status Updated',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a build status is updated for a Bitbucket commit',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_build_status_updated',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Build Status Updated'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_build_status_updated'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_build_status_updated'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_build_status_updated,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketCommitCommentCreatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_commit_comment_created',
|
||||
name: 'Bitbucket Commit Comment Created',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a comment is created on a Bitbucket commit',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_commit_comment_created',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Commit Comment Created'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_commit_comment_created'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_commit_comment_created'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_commit_comment_created,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export { bitbucketBuildStatusCreatedTrigger } from '@/triggers/bitbucket/build_status_created'
|
||||
export { bitbucketBuildStatusUpdatedTrigger } from '@/triggers/bitbucket/build_status_updated'
|
||||
export { bitbucketCommitCommentCreatedTrigger } from '@/triggers/bitbucket/commit_comment_created'
|
||||
export { bitbucketPullRequestApprovalRemovedTrigger } from '@/triggers/bitbucket/pull_request_approval_removed'
|
||||
export { bitbucketPullRequestApprovedTrigger } from '@/triggers/bitbucket/pull_request_approved'
|
||||
export { bitbucketPullRequestChangesRequestRemovedTrigger } from '@/triggers/bitbucket/pull_request_changes_request_removed'
|
||||
export { bitbucketPullRequestChangesRequestedTrigger } from '@/triggers/bitbucket/pull_request_changes_requested'
|
||||
export { bitbucketPullRequestCommentCreatedTrigger } from '@/triggers/bitbucket/pull_request_comment_created'
|
||||
export { bitbucketPullRequestCommentDeletedTrigger } from '@/triggers/bitbucket/pull_request_comment_deleted'
|
||||
export { bitbucketPullRequestCommentReopenedTrigger } from '@/triggers/bitbucket/pull_request_comment_reopened'
|
||||
export { bitbucketPullRequestCommentResolvedTrigger } from '@/triggers/bitbucket/pull_request_comment_resolved'
|
||||
export { bitbucketPullRequestCommentUpdatedTrigger } from '@/triggers/bitbucket/pull_request_comment_updated'
|
||||
export { bitbucketPullRequestCreatedTrigger } from '@/triggers/bitbucket/pull_request_created'
|
||||
export { bitbucketPullRequestDeclinedTrigger } from '@/triggers/bitbucket/pull_request_declined'
|
||||
export { bitbucketPullRequestMergedTrigger } from '@/triggers/bitbucket/pull_request_merged'
|
||||
export { bitbucketPullRequestUpdatedTrigger } from '@/triggers/bitbucket/pull_request_updated'
|
||||
export { bitbucketPushTrigger } from '@/triggers/bitbucket/push'
|
||||
export { bitbucketRepositoryForkedTrigger } from '@/triggers/bitbucket/repository_forked'
|
||||
export { bitbucketRepositoryUpdatedTrigger } from '@/triggers/bitbucket/repository_updated'
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestApprovalRemovedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_approval_removed',
|
||||
name: 'Bitbucket Pull Request Approval Removed',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when approval is removed from a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_approval_removed',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Approval Removed'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_approval_removed'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_approval_removed'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_approval_removed,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestApprovedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_approved',
|
||||
name: 'Bitbucket Pull Request Approved',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket pull request is approved',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_approved',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Approved'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_approved'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_approved'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_approved,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestChangesRequestRemovedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_changes_request_removed',
|
||||
name: 'Bitbucket Pull Request Changes Request Removed',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a changes request is removed from a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_changes_request_removed',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Changes Request Removed'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_changes_request_removed'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_changes_request_removed'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_changes_request_removed,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestChangesRequestedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_changes_requested',
|
||||
name: 'Bitbucket Pull Request Changes Requested',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when changes are requested on a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_changes_requested',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Changes Requested'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_changes_requested'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_changes_requested'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_changes_requested,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCommentCreatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_comment_created',
|
||||
name: 'Bitbucket Pull Request Comment Created',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a comment is created on a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_comment_created',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Comment Created'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_comment_created'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_comment_created'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_comment_created,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCommentDeletedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_comment_deleted',
|
||||
name: 'Bitbucket Pull Request Comment Deleted',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a comment is deleted from a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_comment_deleted',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Comment Deleted'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_comment_deleted'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_comment_deleted'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_comment_deleted,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCommentReopenedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_comment_reopened',
|
||||
name: 'Bitbucket Pull Request Comment Reopened',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a resolved comment is reopened on a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_comment_reopened',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Comment Reopened'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_comment_reopened'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_comment_reopened'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_comment_reopened,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCommentResolvedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_comment_resolved',
|
||||
name: 'Bitbucket Pull Request Comment Resolved',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a comment is resolved on a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_comment_resolved',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Comment Resolved'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_comment_resolved'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_comment_resolved'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_comment_resolved,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCommentUpdatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_comment_updated',
|
||||
name: 'Bitbucket Pull Request Comment Updated',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a comment is updated on a Bitbucket pull request',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_comment_updated',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Comment Updated'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_comment_updated'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_comment_updated'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_comment_updated,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestCreatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_created',
|
||||
name: 'Bitbucket Pull Request Created',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket pull request is created',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_created',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Created'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_created'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_created'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_created,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestDeclinedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_declined',
|
||||
name: 'Bitbucket Pull Request Declined',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket pull request is declined',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_declined',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Declined'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_declined'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_declined'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_declined,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestMergedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_merged',
|
||||
name: 'Bitbucket Pull Request Merged',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket pull request is merged',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_merged',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Merged'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_merged'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_merged'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_merged,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPullRequestUpdatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_pull_request_updated',
|
||||
name: 'Bitbucket Pull Request Updated',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket pull request is updated',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_pull_request_updated',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Pull Request Updated'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_pull_request_updated'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_pull_request_updated'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_pull_request_updated,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketPushTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_push',
|
||||
name: 'Bitbucket Push',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when commits are pushed to a Bitbucket repository',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_push',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
includeDropdown: true,
|
||||
setupInstructions: bitbucketSetupInstructions('Push'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_push'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_push'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_push,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketRepositoryForkedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_repository_forked',
|
||||
name: 'Bitbucket Repository Forked',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket repository is forked',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_repository_forked',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Repository Forked'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_repository_forked'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_repository_forked'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_repository_forked,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitbucketIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketSetupInstructions,
|
||||
bitbucketTriggerOptions,
|
||||
buildBitbucketExtraFields,
|
||||
buildBitbucketOutputs,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
export const bitbucketRepositoryUpdatedTrigger: TriggerConfig = {
|
||||
id: 'bitbucket_repository_updated',
|
||||
name: 'Bitbucket Repository Updated',
|
||||
provider: 'bitbucket',
|
||||
description: 'Trigger workflow when a Bitbucket repository is updated',
|
||||
version: '1.0.0',
|
||||
icon: BitbucketIcon,
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'bitbucket_repository_updated',
|
||||
triggerOptions: [...bitbucketTriggerOptions],
|
||||
setupInstructions: bitbucketSetupInstructions('Repository Updated'),
|
||||
extraFields: buildBitbucketExtraFields('bitbucket_repository_updated'),
|
||||
}),
|
||||
outputs: buildBitbucketOutputs('bitbucket_repository_updated'),
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Event-Key': BITBUCKET_TRIGGER_EVENT_MAP.bitbucket_repository_updated,
|
||||
'X-Hub-Signature': 'sha256=...',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bitbucketBuildStatusCreatedTrigger,
|
||||
bitbucketBuildStatusUpdatedTrigger,
|
||||
bitbucketCommitCommentCreatedTrigger,
|
||||
bitbucketPullRequestApprovalRemovedTrigger,
|
||||
bitbucketPullRequestApprovedTrigger,
|
||||
bitbucketPullRequestChangesRequestedTrigger,
|
||||
bitbucketPullRequestChangesRequestRemovedTrigger,
|
||||
bitbucketPullRequestCommentCreatedTrigger,
|
||||
bitbucketPullRequestCommentDeletedTrigger,
|
||||
bitbucketPullRequestCommentReopenedTrigger,
|
||||
bitbucketPullRequestCommentResolvedTrigger,
|
||||
bitbucketPullRequestCommentUpdatedTrigger,
|
||||
bitbucketPullRequestCreatedTrigger,
|
||||
bitbucketPullRequestDeclinedTrigger,
|
||||
bitbucketPullRequestMergedTrigger,
|
||||
bitbucketPullRequestUpdatedTrigger,
|
||||
bitbucketPushTrigger,
|
||||
bitbucketRepositoryForkedTrigger,
|
||||
bitbucketRepositoryUpdatedTrigger,
|
||||
} from '@/triggers/bitbucket'
|
||||
import {
|
||||
BITBUCKET_TRIGGER_EVENT_MAP,
|
||||
bitbucketTriggerOptions,
|
||||
getBitbucketEventForTrigger,
|
||||
isBitbucketEventMatch,
|
||||
} from '@/triggers/bitbucket/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
const triggers: TriggerConfig[] = [
|
||||
bitbucketPushTrigger,
|
||||
bitbucketRepositoryForkedTrigger,
|
||||
bitbucketRepositoryUpdatedTrigger,
|
||||
bitbucketCommitCommentCreatedTrigger,
|
||||
bitbucketBuildStatusCreatedTrigger,
|
||||
bitbucketBuildStatusUpdatedTrigger,
|
||||
bitbucketPullRequestCreatedTrigger,
|
||||
bitbucketPullRequestUpdatedTrigger,
|
||||
bitbucketPullRequestApprovedTrigger,
|
||||
bitbucketPullRequestApprovalRemovedTrigger,
|
||||
bitbucketPullRequestChangesRequestedTrigger,
|
||||
bitbucketPullRequestChangesRequestRemovedTrigger,
|
||||
bitbucketPullRequestMergedTrigger,
|
||||
bitbucketPullRequestDeclinedTrigger,
|
||||
bitbucketPullRequestCommentCreatedTrigger,
|
||||
bitbucketPullRequestCommentUpdatedTrigger,
|
||||
bitbucketPullRequestCommentDeletedTrigger,
|
||||
bitbucketPullRequestCommentResolvedTrigger,
|
||||
bitbucketPullRequestCommentReopenedTrigger,
|
||||
]
|
||||
|
||||
const commonOutputKeys = [
|
||||
'actor',
|
||||
'attemptNumber',
|
||||
'eventType',
|
||||
'hookUuid',
|
||||
'payload',
|
||||
'repository',
|
||||
'requestUuid',
|
||||
]
|
||||
|
||||
describe('Bitbucket trigger definitions', () => {
|
||||
it('keeps options, event mappings, and trigger configs aligned', () => {
|
||||
const triggerIds = triggers.map((trigger) => trigger.id)
|
||||
|
||||
expect(triggerIds).toEqual(bitbucketTriggerOptions.map((option) => option.id))
|
||||
expect(triggerIds).toEqual(Object.keys(BITBUCKET_TRIGGER_EVENT_MAP))
|
||||
expect(new Set(triggerIds)).toHaveLength(19)
|
||||
|
||||
for (const trigger of triggers) {
|
||||
const eventKey = getBitbucketEventForTrigger(trigger.id)
|
||||
expect(eventKey).toBeDefined()
|
||||
expect(trigger.provider).toBe('bitbucket')
|
||||
expect(trigger.version).toBe('1.0.0')
|
||||
expect(trigger.webhook?.headers?.['X-Event-Key']).toBe(eventKey)
|
||||
expect(isBitbucketEventMatch(trigger.id, eventKey)).toBe(true)
|
||||
expect(isBitbucketEventMatch(trigger.id, 'unrelated:event')).toBe(false)
|
||||
}
|
||||
|
||||
expect(getBitbucketEventForTrigger('bitbucket_unknown')).toBeUndefined()
|
||||
expect(isBitbucketEventMatch('bitbucket_unknown', 'repo:push')).toBe(false)
|
||||
})
|
||||
|
||||
it('includes the trigger dropdown only on the primary push trigger', () => {
|
||||
const dropdownOwners = triggers
|
||||
.filter((trigger) =>
|
||||
trigger.subBlocks.some((subBlock) => subBlock.id === 'selectedTriggerId')
|
||||
)
|
||||
.map((trigger) => trigger.id)
|
||||
|
||||
expect(dropdownOwners).toEqual(['bitbucket_push'])
|
||||
})
|
||||
|
||||
it('uses the canonical OAuth, workspace, and repository configuration', () => {
|
||||
for (const trigger of triggers) {
|
||||
const credential = trigger.subBlocks.find((subBlock) => subBlock.id === 'triggerCredentials')
|
||||
const workspace = trigger.subBlocks.find((subBlock) => subBlock.id === 'workspacePicker')
|
||||
const manualWorkspace = trigger.subBlocks.find(
|
||||
(subBlock) => subBlock.id === 'workspaceSlugInput'
|
||||
)
|
||||
const repository = trigger.subBlocks.find((subBlock) => subBlock.id === 'repositoryPicker')
|
||||
const manualRepository = trigger.subBlocks.find(
|
||||
(subBlock) => subBlock.id === 'repositorySlugInput'
|
||||
)
|
||||
|
||||
expect(credential).toMatchObject({
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
serviceId: 'bitbucket',
|
||||
requiredScopes: ['account', 'repository', 'pullrequest', 'webhook'],
|
||||
required: true,
|
||||
})
|
||||
expect(workspace).toMatchObject({
|
||||
canonicalParamId: 'workspaceSlug',
|
||||
selectorKey: 'bitbucket.workspaces',
|
||||
dependsOn: ['triggerCredentials'],
|
||||
required: true,
|
||||
})
|
||||
expect(manualWorkspace).toMatchObject({
|
||||
canonicalParamId: 'workspaceSlug',
|
||||
mode: 'trigger-advanced',
|
||||
required: true,
|
||||
})
|
||||
expect(repository).toMatchObject({
|
||||
canonicalParamId: 'repoSlug',
|
||||
selectorKey: 'bitbucket.repositories',
|
||||
dependsOn: ['triggerCredentials', 'workspacePicker'],
|
||||
required: true,
|
||||
})
|
||||
expect(manualRepository).toMatchObject({
|
||||
canonicalParamId: 'repoSlug',
|
||||
mode: 'trigger-advanced',
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('declares the common outputs on every event and only its documented family outputs', () => {
|
||||
const expectedFamilyOutputs: Record<string, string[]> = {
|
||||
bitbucket_push: ['push'],
|
||||
bitbucket_repository_forked: ['fork'],
|
||||
bitbucket_repository_updated: ['changes'],
|
||||
bitbucket_commit_comment_created: ['comment', 'commentContent', 'commentId', 'commit'],
|
||||
bitbucket_build_status_created: [
|
||||
'commitHash',
|
||||
'commitStatus',
|
||||
'statusKey',
|
||||
'statusName',
|
||||
'statusState',
|
||||
'statusUrl',
|
||||
],
|
||||
bitbucket_build_status_updated: [
|
||||
'commitHash',
|
||||
'commitStatus',
|
||||
'statusKey',
|
||||
'statusName',
|
||||
'statusState',
|
||||
'statusUrl',
|
||||
],
|
||||
}
|
||||
const pullRequestOutputs = [
|
||||
'destinationBranch',
|
||||
'pullRequest',
|
||||
'pullRequestId',
|
||||
'pullRequestState',
|
||||
'pullRequestTitle',
|
||||
'sourceBranch',
|
||||
]
|
||||
|
||||
for (const trigger of triggers) {
|
||||
let familyOutputs = expectedFamilyOutputs[trigger.id]
|
||||
if (!familyOutputs) {
|
||||
familyOutputs = [...pullRequestOutputs]
|
||||
if (
|
||||
trigger.id === 'bitbucket_pull_request_approved' ||
|
||||
trigger.id === 'bitbucket_pull_request_approval_removed'
|
||||
) {
|
||||
familyOutputs.push('approval')
|
||||
} else if (
|
||||
trigger.id === 'bitbucket_pull_request_changes_requested' ||
|
||||
trigger.id === 'bitbucket_pull_request_changes_request_removed'
|
||||
) {
|
||||
familyOutputs.push('changesRequest')
|
||||
} else if (trigger.id.startsWith('bitbucket_pull_request_comment_')) {
|
||||
familyOutputs.push('comment', 'commentContent', 'commentId')
|
||||
}
|
||||
}
|
||||
|
||||
expect(Object.keys(trigger.outputs).sort()).toEqual(
|
||||
[...commonOutputKeys, ...familyOutputs].sort()
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,359 @@
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { TriggerOutput } from '@/triggers/types'
|
||||
|
||||
export const bitbucketTriggerOptions = [
|
||||
{ label: 'Push', id: 'bitbucket_push' },
|
||||
{ label: 'Repository Forked', id: 'bitbucket_repository_forked' },
|
||||
{ label: 'Repository Updated', id: 'bitbucket_repository_updated' },
|
||||
{ label: 'Commit Comment Created', id: 'bitbucket_commit_comment_created' },
|
||||
{ label: 'Build Status Created', id: 'bitbucket_build_status_created' },
|
||||
{ label: 'Build Status Updated', id: 'bitbucket_build_status_updated' },
|
||||
{ label: 'Pull Request Created', id: 'bitbucket_pull_request_created' },
|
||||
{ label: 'Pull Request Updated', id: 'bitbucket_pull_request_updated' },
|
||||
{ label: 'Pull Request Approved', id: 'bitbucket_pull_request_approved' },
|
||||
{
|
||||
label: 'Pull Request Approval Removed',
|
||||
id: 'bitbucket_pull_request_approval_removed',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Changes Requested',
|
||||
id: 'bitbucket_pull_request_changes_requested',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Changes Request Removed',
|
||||
id: 'bitbucket_pull_request_changes_request_removed',
|
||||
},
|
||||
{ label: 'Pull Request Merged', id: 'bitbucket_pull_request_merged' },
|
||||
{ label: 'Pull Request Declined', id: 'bitbucket_pull_request_declined' },
|
||||
{
|
||||
label: 'Pull Request Comment Created',
|
||||
id: 'bitbucket_pull_request_comment_created',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Comment Updated',
|
||||
id: 'bitbucket_pull_request_comment_updated',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Comment Deleted',
|
||||
id: 'bitbucket_pull_request_comment_deleted',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Comment Resolved',
|
||||
id: 'bitbucket_pull_request_comment_resolved',
|
||||
},
|
||||
{
|
||||
label: 'Pull Request Comment Reopened',
|
||||
id: 'bitbucket_pull_request_comment_reopened',
|
||||
},
|
||||
] as const
|
||||
|
||||
/** Maps Sim trigger IDs to the exact Bitbucket Cloud repository-hook event keys. */
|
||||
export const BITBUCKET_TRIGGER_EVENT_MAP = {
|
||||
bitbucket_push: 'repo:push',
|
||||
bitbucket_repository_forked: 'repo:fork',
|
||||
bitbucket_repository_updated: 'repo:updated',
|
||||
bitbucket_commit_comment_created: 'repo:commit_comment_created',
|
||||
bitbucket_build_status_created: 'repo:commit_status_created',
|
||||
bitbucket_build_status_updated: 'repo:commit_status_updated',
|
||||
bitbucket_pull_request_created: 'pullrequest:created',
|
||||
bitbucket_pull_request_updated: 'pullrequest:updated',
|
||||
bitbucket_pull_request_approved: 'pullrequest:approved',
|
||||
bitbucket_pull_request_approval_removed: 'pullrequest:unapproved',
|
||||
bitbucket_pull_request_changes_requested: 'pullrequest:changes_request_created',
|
||||
bitbucket_pull_request_changes_request_removed: 'pullrequest:changes_request_removed',
|
||||
bitbucket_pull_request_merged: 'pullrequest:fulfilled',
|
||||
bitbucket_pull_request_declined: 'pullrequest:rejected',
|
||||
bitbucket_pull_request_comment_created: 'pullrequest:comment_created',
|
||||
bitbucket_pull_request_comment_updated: 'pullrequest:comment_updated',
|
||||
bitbucket_pull_request_comment_deleted: 'pullrequest:comment_deleted',
|
||||
bitbucket_pull_request_comment_resolved: 'pullrequest:comment_resolved',
|
||||
bitbucket_pull_request_comment_reopened: 'pullrequest:comment_reopened',
|
||||
} as const satisfies Record<string, string>
|
||||
|
||||
export type BitbucketTriggerId = keyof typeof BITBUCKET_TRIGGER_EVENT_MAP
|
||||
|
||||
export function getBitbucketEventForTrigger(triggerId: string): string | undefined {
|
||||
return BITBUCKET_TRIGGER_EVENT_MAP[triggerId as BitbucketTriggerId]
|
||||
}
|
||||
|
||||
export function isBitbucketEventMatch(triggerId: string, eventKey: string | undefined): boolean {
|
||||
const expectedEvent = getBitbucketEventForTrigger(triggerId)
|
||||
return Boolean(expectedEvent && eventKey && expectedEvent === eventKey)
|
||||
}
|
||||
|
||||
export function bitbucketSetupInstructions(eventLabel: string): string {
|
||||
const instructions = [
|
||||
'Connect the <strong>Bitbucket account</strong> that administers the repository.',
|
||||
'Select the <strong>workspace</strong> and <strong>repository</strong> to monitor.',
|
||||
`Deploy the workflow. Sim automatically creates a signed webhook for <strong>${eventLabel}</strong> events.`,
|
||||
'Bitbucket allows up to <strong>50 webhooks per repository</strong>; each deployed Sim trigger uses one.',
|
||||
'Undeploying the workflow automatically removes the webhook from Bitbucket.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
/** Fields shared by every automatically managed Bitbucket repository trigger. */
|
||||
export function buildBitbucketExtraFields(triggerId: BitbucketTriggerId): SubBlockConfig[] {
|
||||
const condition = { field: 'selectedTriggerId', value: triggerId }
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'triggerCredentials',
|
||||
title: 'Bitbucket Account',
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
serviceId: 'bitbucket',
|
||||
requiredScopes: ['account', 'repository', 'pullrequest', 'webhook'],
|
||||
placeholder: 'Select Bitbucket account',
|
||||
description:
|
||||
'The Bitbucket account used to create and remove the repository webhook. Repository administrator access is required.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition,
|
||||
},
|
||||
{
|
||||
id: 'workspacePicker',
|
||||
title: 'Workspace',
|
||||
canvasNoun: 'a workspace',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'workspaceSlug',
|
||||
serviceId: 'bitbucket',
|
||||
selectorKey: 'bitbucket.workspaces',
|
||||
dependsOn: ['triggerCredentials'],
|
||||
placeholder: 'Select Bitbucket workspace',
|
||||
description: 'The workspace containing the repository to monitor.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition,
|
||||
},
|
||||
{
|
||||
id: 'workspaceSlugInput',
|
||||
title: 'Workspace Slug',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'workspaceSlug',
|
||||
placeholder: 'Enter workspace slug',
|
||||
description: 'Enter the workspace slug directly instead of selecting a workspace.',
|
||||
required: true,
|
||||
mode: 'trigger-advanced',
|
||||
condition,
|
||||
},
|
||||
{
|
||||
id: 'repositoryPicker',
|
||||
title: 'Repository',
|
||||
canvasNoun: 'a repository',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'repoSlug',
|
||||
serviceId: 'bitbucket',
|
||||
selectorKey: 'bitbucket.repositories',
|
||||
dependsOn: ['triggerCredentials', 'workspacePicker'],
|
||||
placeholder: 'Select Bitbucket repository',
|
||||
description: 'The repository where Sim creates the webhook.',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition,
|
||||
},
|
||||
{
|
||||
id: 'repositorySlugInput',
|
||||
title: 'Repository Slug',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'repoSlug',
|
||||
placeholder: 'Enter repository slug',
|
||||
description: 'Enter the repository slug directly instead of selecting a repository.',
|
||||
required: true,
|
||||
mode: 'trigger-advanced',
|
||||
condition,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function buildCommonOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
eventType: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket event key from X-Event-Key',
|
||||
},
|
||||
hookUuid: {
|
||||
type: 'string',
|
||||
description: 'UUID of the Bitbucket repository webhook',
|
||||
},
|
||||
requestUuid: {
|
||||
type: 'string',
|
||||
description: 'Bitbucket delivery request UUID',
|
||||
},
|
||||
attemptNumber: {
|
||||
type: 'number',
|
||||
description: 'Bitbucket delivery attempt number',
|
||||
},
|
||||
actor: { type: 'json', description: 'User or app that caused the event' },
|
||||
repository: {
|
||||
type: 'json',
|
||||
description: 'Repository where the event occurred',
|
||||
},
|
||||
payload: {
|
||||
type: 'json',
|
||||
description: 'Full parsed Bitbucket webhook payload',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildCommentOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
comment: { type: 'json', description: 'Bitbucket comment object' },
|
||||
commentId: { type: 'number', description: 'Repository-scoped comment ID' },
|
||||
commentContent: {
|
||||
type: 'string',
|
||||
description: 'Raw text content of the comment',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function buildPullRequestOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
pullRequest: { type: 'json', description: 'Bitbucket pull request object' },
|
||||
pullRequestId: {
|
||||
type: 'number',
|
||||
description: 'Repository-scoped pull request ID',
|
||||
},
|
||||
pullRequestTitle: { type: 'string', description: 'Pull request title' },
|
||||
pullRequestState: { type: 'string', description: 'Pull request state' },
|
||||
sourceBranch: { type: 'string', description: 'Pull request source branch' },
|
||||
destinationBranch: {
|
||||
type: 'string',
|
||||
description: 'Pull request destination branch',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketPushOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
push: {
|
||||
type: 'json',
|
||||
description: 'Push details, including changes and commits',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketForkOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
fork: { type: 'json', description: 'Newly created repository fork' },
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketRepositoryUpdatedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
changes: {
|
||||
type: 'json',
|
||||
description: 'Repository fields changed by the update',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketCommitCommentOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
...buildCommentOutputs(),
|
||||
commit: { type: 'json', description: 'Commit the comment was created on' },
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketBuildStatusOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
commitStatus: {
|
||||
type: 'json',
|
||||
description: 'Bitbucket commit status object',
|
||||
},
|
||||
commitHash: {
|
||||
type: 'string',
|
||||
description: 'Hash of the commit with this status',
|
||||
},
|
||||
statusKey: {
|
||||
type: 'string',
|
||||
description: 'Key identifying the build status',
|
||||
},
|
||||
statusState: { type: 'string', description: 'Current build status state' },
|
||||
statusName: {
|
||||
type: 'string',
|
||||
description: 'Display name of the build status',
|
||||
},
|
||||
statusUrl: {
|
||||
type: 'string',
|
||||
description: 'URL associated with the build status',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketPullRequestOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildCommonOutputs(),
|
||||
...buildPullRequestOutputs(),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketPullRequestApprovalOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBitbucketPullRequestOutputs(),
|
||||
approval: { type: 'json', description: 'Pull request approval details' },
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketPullRequestChangesRequestOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBitbucketPullRequestOutputs(),
|
||||
changesRequest: {
|
||||
type: 'json',
|
||||
description: 'Pull request changes-request details',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketPullRequestCommentOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBitbucketPullRequestOutputs(),
|
||||
...buildCommentOutputs(),
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBitbucketOutputs(
|
||||
triggerId: BitbucketTriggerId
|
||||
): Record<string, TriggerOutput> {
|
||||
switch (triggerId) {
|
||||
case 'bitbucket_push':
|
||||
return buildBitbucketPushOutputs()
|
||||
case 'bitbucket_repository_forked':
|
||||
return buildBitbucketForkOutputs()
|
||||
case 'bitbucket_repository_updated':
|
||||
return buildBitbucketRepositoryUpdatedOutputs()
|
||||
case 'bitbucket_commit_comment_created':
|
||||
return buildBitbucketCommitCommentOutputs()
|
||||
case 'bitbucket_build_status_created':
|
||||
case 'bitbucket_build_status_updated':
|
||||
return buildBitbucketBuildStatusOutputs()
|
||||
case 'bitbucket_pull_request_approved':
|
||||
case 'bitbucket_pull_request_approval_removed':
|
||||
return buildBitbucketPullRequestApprovalOutputs()
|
||||
case 'bitbucket_pull_request_changes_requested':
|
||||
case 'bitbucket_pull_request_changes_request_removed':
|
||||
return buildBitbucketPullRequestChangesRequestOutputs()
|
||||
case 'bitbucket_pull_request_comment_created':
|
||||
case 'bitbucket_pull_request_comment_updated':
|
||||
case 'bitbucket_pull_request_comment_deleted':
|
||||
case 'bitbucket_pull_request_comment_resolved':
|
||||
case 'bitbucket_pull_request_comment_reopened':
|
||||
return buildBitbucketPullRequestCommentOutputs()
|
||||
case 'bitbucket_pull_request_created':
|
||||
case 'bitbucket_pull_request_updated':
|
||||
case 'bitbucket_pull_request_merged':
|
||||
case 'bitbucket_pull_request_declined':
|
||||
return buildBitbucketPullRequestOutputs()
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,27 @@ import {
|
||||
azureDevOpsWebhookTrigger,
|
||||
azureDevOpsWorkItemCreatedTrigger,
|
||||
} from '@/triggers/azure_devops'
|
||||
import {
|
||||
bitbucketBuildStatusCreatedTrigger,
|
||||
bitbucketBuildStatusUpdatedTrigger,
|
||||
bitbucketCommitCommentCreatedTrigger,
|
||||
bitbucketPullRequestApprovalRemovedTrigger,
|
||||
bitbucketPullRequestApprovedTrigger,
|
||||
bitbucketPullRequestChangesRequestedTrigger,
|
||||
bitbucketPullRequestChangesRequestRemovedTrigger,
|
||||
bitbucketPullRequestCommentCreatedTrigger,
|
||||
bitbucketPullRequestCommentDeletedTrigger,
|
||||
bitbucketPullRequestCommentReopenedTrigger,
|
||||
bitbucketPullRequestCommentResolvedTrigger,
|
||||
bitbucketPullRequestCommentUpdatedTrigger,
|
||||
bitbucketPullRequestCreatedTrigger,
|
||||
bitbucketPullRequestDeclinedTrigger,
|
||||
bitbucketPullRequestMergedTrigger,
|
||||
bitbucketPullRequestUpdatedTrigger,
|
||||
bitbucketPushTrigger,
|
||||
bitbucketRepositoryForkedTrigger,
|
||||
bitbucketRepositoryUpdatedTrigger,
|
||||
} from '@/triggers/bitbucket'
|
||||
import {
|
||||
calcomBookingCancelledTrigger,
|
||||
calcomBookingCreatedTrigger,
|
||||
@@ -513,6 +534,25 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
attio_list_updated: attioListUpdatedTrigger,
|
||||
attio_list_deleted: attioListDeletedTrigger,
|
||||
attio_workspace_member_created: attioWorkspaceMemberCreatedTrigger,
|
||||
bitbucket_push: bitbucketPushTrigger,
|
||||
bitbucket_repository_forked: bitbucketRepositoryForkedTrigger,
|
||||
bitbucket_repository_updated: bitbucketRepositoryUpdatedTrigger,
|
||||
bitbucket_commit_comment_created: bitbucketCommitCommentCreatedTrigger,
|
||||
bitbucket_build_status_created: bitbucketBuildStatusCreatedTrigger,
|
||||
bitbucket_build_status_updated: bitbucketBuildStatusUpdatedTrigger,
|
||||
bitbucket_pull_request_created: bitbucketPullRequestCreatedTrigger,
|
||||
bitbucket_pull_request_updated: bitbucketPullRequestUpdatedTrigger,
|
||||
bitbucket_pull_request_approved: bitbucketPullRequestApprovedTrigger,
|
||||
bitbucket_pull_request_approval_removed: bitbucketPullRequestApprovalRemovedTrigger,
|
||||
bitbucket_pull_request_changes_requested: bitbucketPullRequestChangesRequestedTrigger,
|
||||
bitbucket_pull_request_changes_request_removed: bitbucketPullRequestChangesRequestRemovedTrigger,
|
||||
bitbucket_pull_request_merged: bitbucketPullRequestMergedTrigger,
|
||||
bitbucket_pull_request_declined: bitbucketPullRequestDeclinedTrigger,
|
||||
bitbucket_pull_request_comment_created: bitbucketPullRequestCommentCreatedTrigger,
|
||||
bitbucket_pull_request_comment_updated: bitbucketPullRequestCommentUpdatedTrigger,
|
||||
bitbucket_pull_request_comment_deleted: bitbucketPullRequestCommentDeletedTrigger,
|
||||
bitbucket_pull_request_comment_resolved: bitbucketPullRequestCommentResolvedTrigger,
|
||||
bitbucket_pull_request_comment_reopened: bitbucketPullRequestCommentReopenedTrigger,
|
||||
calendly_webhook: calendlyWebhookTrigger,
|
||||
calendly_invitee_created: calendlyInviteeCreatedTrigger,
|
||||
calendly_invitee_canceled: calendlyInviteeCanceledTrigger,
|
||||
|
||||
@@ -2807,7 +2807,7 @@
|
||||
"slug": "bitbucket",
|
||||
"name": "Bitbucket",
|
||||
"description": "Work with Bitbucket Cloud repositories, pull requests, and pipelines",
|
||||
"longDescription": "Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, and diagnose or control pipelines. This action integration uses OAuth and does not create webhooks or triggers.",
|
||||
"longDescription": "Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, diagnose or control pipelines, and start workflows from repository and pull request events. OAuth is used for actions and automatic webhook management.",
|
||||
"bgColor": "#FFFFFF",
|
||||
"iconName": "BitbucketIcon",
|
||||
"docsUrl": "https://docs.sim.ai/integrations/bitbucket",
|
||||
@@ -2934,8 +2934,104 @@
|
||||
}
|
||||
],
|
||||
"operationCount": 30,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"triggers": [
|
||||
{
|
||||
"id": "bitbucket_push",
|
||||
"name": "Bitbucket Push",
|
||||
"description": "Trigger workflow when commits are pushed to a Bitbucket repository"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_repository_forked",
|
||||
"name": "Bitbucket Repository Forked",
|
||||
"description": "Trigger workflow when a Bitbucket repository is forked"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_repository_updated",
|
||||
"name": "Bitbucket Repository Updated",
|
||||
"description": "Trigger workflow when a Bitbucket repository is updated"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_commit_comment_created",
|
||||
"name": "Bitbucket Commit Comment Created",
|
||||
"description": "Trigger workflow when a comment is created on a Bitbucket commit"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_build_status_created",
|
||||
"name": "Bitbucket Build Status Created",
|
||||
"description": "Trigger workflow when a build status is created for a Bitbucket commit"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_build_status_updated",
|
||||
"name": "Bitbucket Build Status Updated",
|
||||
"description": "Trigger workflow when a build status is updated for a Bitbucket commit"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_created",
|
||||
"name": "Bitbucket Pull Request Created",
|
||||
"description": "Trigger workflow when a Bitbucket pull request is created"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_updated",
|
||||
"name": "Bitbucket Pull Request Updated",
|
||||
"description": "Trigger workflow when a Bitbucket pull request is updated"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_approved",
|
||||
"name": "Bitbucket Pull Request Approved",
|
||||
"description": "Trigger workflow when a Bitbucket pull request is approved"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_approval_removed",
|
||||
"name": "Bitbucket Pull Request Approval Removed",
|
||||
"description": "Trigger workflow when approval is removed from a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_changes_requested",
|
||||
"name": "Bitbucket Pull Request Changes Requested",
|
||||
"description": "Trigger workflow when changes are requested on a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_changes_request_removed",
|
||||
"name": "Bitbucket Pull Request Changes Request Removed",
|
||||
"description": "Trigger workflow when a changes request is removed from a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_merged",
|
||||
"name": "Bitbucket Pull Request Merged",
|
||||
"description": "Trigger workflow when a Bitbucket pull request is merged"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_declined",
|
||||
"name": "Bitbucket Pull Request Declined",
|
||||
"description": "Trigger workflow when a Bitbucket pull request is declined"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_comment_created",
|
||||
"name": "Bitbucket Pull Request Comment Created",
|
||||
"description": "Trigger workflow when a comment is created on a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_comment_updated",
|
||||
"name": "Bitbucket Pull Request Comment Updated",
|
||||
"description": "Trigger workflow when a comment is updated on a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_comment_deleted",
|
||||
"name": "Bitbucket Pull Request Comment Deleted",
|
||||
"description": "Trigger workflow when a comment is deleted from a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_comment_resolved",
|
||||
"name": "Bitbucket Pull Request Comment Resolved",
|
||||
"description": "Trigger workflow when a comment is resolved on a Bitbucket pull request"
|
||||
},
|
||||
{
|
||||
"id": "bitbucket_pull_request_comment_reopened",
|
||||
"name": "Bitbucket Pull Request Comment Reopened",
|
||||
"description": "Trigger workflow when a resolved comment is reopened on a Bitbucket pull request"
|
||||
}
|
||||
],
|
||||
"triggerCount": 19,
|
||||
"authType": "oauth",
|
||||
"oauthServiceId": "bitbucket",
|
||||
"category": "tools",
|
||||
|
||||
@@ -10,29 +10,29 @@
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/chat/[chatId]/page.tsx": {
|
||||
"modules": 3050,
|
||||
"modules": 3073,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/home/home.tsx": 1383,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx": 1033,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts": 894,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts": 891,
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx": 334,
|
||||
"apps/sim/blocks/registry.ts": 315,
|
||||
"apps/sim/lib/auth/index.ts": 228
|
||||
"apps/sim/lib/auth/index.ts": 230
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/files/[fileId]/page.tsx": {
|
||||
"modules": 2007,
|
||||
"modules": 2030,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/blocks/registry.ts": 341,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/files.tsx": 273,
|
||||
"apps/sim/lib/auth/index.ts": 234,
|
||||
"apps/sim/lib/auth/index.ts": 236,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts": 151,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx": 134,
|
||||
"apps/sim/lib/api/contracts/index.ts": 109,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 107
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 109
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/files/[fileId]/view/page.tsx": {
|
||||
@@ -43,16 +43,16 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/files/page.tsx": {
|
||||
"modules": 2007,
|
||||
"modules": 2030,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/blocks/registry.ts": 341,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/files.tsx": 275,
|
||||
"apps/sim/lib/auth/index.ts": 234,
|
||||
"apps/sim/lib/auth/index.ts": 236,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/index.ts": 151,
|
||||
"apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx": 134,
|
||||
"apps/sim/lib/api/contracts/index.ts": 109,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 107
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 109
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/home/layout.tsx": {
|
||||
@@ -60,23 +60,23 @@
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/home/page.tsx": {
|
||||
"modules": 3050,
|
||||
"modules": 3073,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/home/home.tsx": 1383,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx": 1033,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/index.ts": 894,
|
||||
"apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/index.ts": 891,
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx": 334,
|
||||
"apps/sim/blocks/registry.ts": 315,
|
||||
"apps/sim/lib/auth/index.ts": 228
|
||||
"apps/sim/lib/auth/index.ts": 230
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/integrations/[block]/page.tsx": {
|
||||
"modules": 1329,
|
||||
"modules": 1350,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx": 1303,
|
||||
"apps/sim/triggers/index.ts": 488,
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx": 1324,
|
||||
"apps/sim/triggers/index.ts": 509,
|
||||
"apps/sim/blocks/registry.ts": 447,
|
||||
"apps/sim/lib/api/contracts/index.ts": 129,
|
||||
"apps/sim/blocks/blocks/credential-group.ts": 106,
|
||||
@@ -86,10 +86,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/integrations/connected/[credentialId]/page.tsx": {
|
||||
"modules": 1307,
|
||||
"modules": 1328,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx": 1306,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx": 1327,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 348,
|
||||
"apps/sim/lib/api/contracts/index.ts": 135,
|
||||
"apps/sim/stores/workflows/registry/store.ts": 77,
|
||||
@@ -99,11 +99,11 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/integrations/page.tsx": {
|
||||
"modules": 1314,
|
||||
"modules": 1335,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx": 1024,
|
||||
"apps/sim/blocks/registry.ts": 940,
|
||||
"apps/sim/triggers/index.ts": 488,
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx": 1045,
|
||||
"apps/sim/blocks/registry.ts": 961,
|
||||
"apps/sim/triggers/index.ts": 509,
|
||||
"apps/sim/lib/api/contracts/index.ts": 131,
|
||||
"apps/sim/blocks/blocks/credential-group.ts": 107,
|
||||
"apps/sim/stores/workflows/registry/store.ts": 81,
|
||||
@@ -112,10 +112,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/knowledge/[id]/[documentId]/page.tsx": {
|
||||
"modules": 1509,
|
||||
"modules": 1530,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx": 1216,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx": 1237,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 344,
|
||||
"apps/sim/blocks/registry-maps.ts": 341,
|
||||
"apps/sim/lib/api/contracts/index.ts": 129,
|
||||
@@ -125,10 +125,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/knowledge/[id]/page.tsx": {
|
||||
"modules": 1521,
|
||||
"modules": 1542,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx": 1227,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx": 1248,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 349,
|
||||
"apps/sim/blocks/registry-maps.ts": 346,
|
||||
"apps/sim/lib/api/contracts/index.ts": 129,
|
||||
@@ -138,36 +138,36 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/knowledge/page.tsx": {
|
||||
"modules": 2231,
|
||||
"modules": 2253,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/blocks/registry.ts": 339,
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts": 291,
|
||||
"apps/sim/lib/knowledge/application/knowledge-bases.ts": 235,
|
||||
"apps/sim/lib/auth/index.ts": 182,
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts": 290,
|
||||
"apps/sim/lib/knowledge/application/knowledge-bases.ts": 234,
|
||||
"apps/sim/lib/auth/index.ts": 183,
|
||||
"apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx": 154,
|
||||
"apps/sim/lib/knowledge/orchestration/index.ts": 148,
|
||||
"apps/sim/lib/knowledge/orchestration/connectors.ts": 142
|
||||
"apps/sim/lib/knowledge/orchestration/index.ts": 147,
|
||||
"apps/sim/lib/knowledge/orchestration/connectors.ts": 141
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/layout.tsx": {
|
||||
"modules": 2083,
|
||||
"modules": 2106,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/blocks/registry.ts": 340,
|
||||
"apps/sim/lib/auth/index.ts": 307,
|
||||
"apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/index.ts": 305,
|
||||
"apps/sim/lib/auth/index.ts": 305,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx": 297,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts": 205,
|
||||
"apps/sim/lib/api/contracts/index.ts": 109,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 107
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 109
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/logs/page.tsx": {
|
||||
"modules": 1748,
|
||||
"modules": 1769,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/logs/logs.tsx": 1457,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/logs/logs.tsx": 1478,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/app/workspace/[workspaceId]/logs/components/index.ts": 399,
|
||||
"apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts": 345,
|
||||
"apps/sim/blocks/registry.ts": 335,
|
||||
@@ -181,19 +181,19 @@
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/settings/[section]/layout.tsx": {
|
||||
"modules": 4,
|
||||
"modules": 11,
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/settings/[section]/page.tsx": {
|
||||
"modules": 2102,
|
||||
"modules": 2125,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx": 445,
|
||||
"apps/sim/blocks/registry.ts": 339,
|
||||
"apps/sim/lib/auth/index.ts": 314,
|
||||
"apps/sim/lib/auth/index.ts": 316,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 109,
|
||||
"apps/sim/lib/api/contracts/index.ts": 107,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 107,
|
||||
"apps/sim/lib/webhooks/providers/registry.ts": 105,
|
||||
"apps/sim/lib/webhooks/providers/registry.ts": 107,
|
||||
"apps/sim/lib/api/contracts/tools/index.ts": 59
|
||||
}
|
||||
},
|
||||
@@ -202,12 +202,12 @@
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/settings/billing/credit-usage/page.tsx": {
|
||||
"modules": 1664,
|
||||
"modules": 1687,
|
||||
"gateways": {
|
||||
"apps/sim/lib/auth/index.ts": 1527,
|
||||
"apps/sim/lib/auth/index.ts": 1550,
|
||||
"apps/sim/blocks/registry.ts": 616,
|
||||
"apps/sim/blocks/registry-maps.ts": 613,
|
||||
"apps/sim/triggers/index.ts": 453,
|
||||
"apps/sim/triggers/index.ts": 473,
|
||||
"apps/sim/blocks/blocks/credential-group.ts": 271,
|
||||
"apps/sim/stores/workflows/registry/store.ts": 253,
|
||||
"apps/sim/hooks/queries/deployments.ts": 245,
|
||||
@@ -223,9 +223,9 @@
|
||||
"gateways": {}
|
||||
},
|
||||
"app/workspace/[workspaceId]/settings/secrets/[credentialId]/page.tsx": {
|
||||
"modules": 1354,
|
||||
"modules": 1375,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 350,
|
||||
"apps/sim/blocks/registry-maps.ts": 347,
|
||||
"apps/sim/lib/api/contracts/index.ts": 136,
|
||||
@@ -236,10 +236,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/skills/[skillId]/page.tsx": {
|
||||
"modules": 1430,
|
||||
"modules": 1451,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx": 1429,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/[skillId]/skill-detail.tsx": 1450,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 349,
|
||||
"apps/sim/blocks/registry-maps.ts": 347,
|
||||
"apps/sim/lib/api/contracts/index.ts": 127,
|
||||
@@ -249,10 +249,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/skills/new/page.tsx": {
|
||||
"modules": 1428,
|
||||
"modules": 1449,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx": 1427,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx": 1448,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 349,
|
||||
"apps/sim/blocks/registry-maps.ts": 347,
|
||||
"apps/sim/lib/api/contracts/index.ts": 127,
|
||||
@@ -262,24 +262,24 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/skills/page.tsx": {
|
||||
"modules": 1297,
|
||||
"modules": 1318,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/skills.tsx": 1007,
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/index.ts": 995,
|
||||
"apps/sim/blocks/registry.ts": 983,
|
||||
"apps/sim/blocks/registry-maps.ts": 981,
|
||||
"apps/sim/triggers/index.ts": 488,
|
||||
"apps/sim/app/workspace/[workspaceId]/skills/skills.tsx": 1028,
|
||||
"apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/index.ts": 1016,
|
||||
"apps/sim/blocks/registry.ts": 1004,
|
||||
"apps/sim/blocks/registry-maps.ts": 1002,
|
||||
"apps/sim/triggers/index.ts": 509,
|
||||
"apps/sim/lib/api/contracts/index.ts": 136,
|
||||
"apps/sim/blocks/blocks/credential-group.ts": 120,
|
||||
"apps/sim/stores/workflows/registry/store.ts": 92
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/tables/[tableId]/page.tsx": {
|
||||
"modules": 2275,
|
||||
"modules": 2298,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx": 564,
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/lib/auth/index.ts": 336,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/lib/auth/index.ts": 338,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/components/preview/index.ts": 315,
|
||||
"apps/sim/blocks/registry.ts": 315,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/index.ts": 273,
|
||||
@@ -288,15 +288,15 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/tables/page.tsx": {
|
||||
"modules": 1852,
|
||||
"modules": 1875,
|
||||
"gateways": {
|
||||
"apps/sim/triggers/registry.ts": 452,
|
||||
"apps/sim/triggers/registry.ts": 472,
|
||||
"apps/sim/blocks/registry.ts": 340,
|
||||
"apps/sim/lib/auth/index.ts": 325,
|
||||
"apps/sim/lib/auth/index.ts": 327,
|
||||
"apps/sim/lib/api/contracts/index.ts": 116,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 107,
|
||||
"apps/sim/lib/webhooks/providers/index.ts": 109,
|
||||
"apps/sim/lib/webhooks/providers/registry.ts": 107,
|
||||
"apps/sim/app/workspace/[workspaceId]/tables/tables.tsx": 106,
|
||||
"apps/sim/lib/webhooks/providers/registry.ts": 105,
|
||||
"apps/sim/lib/api/contracts/tools/index.ts": 60
|
||||
}
|
||||
},
|
||||
@@ -314,11 +314,11 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/w/[workflowId]/layout.tsx": {
|
||||
"modules": 2242,
|
||||
"modules": 2263,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx": 2241,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx": 2262,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts": 548,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/index.ts": 465,
|
||||
"apps/sim/blocks/registry.ts": 335,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts": 290,
|
||||
@@ -327,10 +327,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/w/[workflowId]/page.tsx": {
|
||||
"modules": 2272,
|
||||
"modules": 2293,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx": 2271,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx": 2292,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 335,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts": 309,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/index.ts": 271,
|
||||
@@ -340,10 +340,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/[workspaceId]/w/page.tsx": {
|
||||
"modules": 2242,
|
||||
"modules": 2263,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts": 955,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/index.ts": 465,
|
||||
"apps/sim/blocks/registry.ts": 335,
|
||||
"apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/index.ts": 290,
|
||||
@@ -353,10 +353,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/layout.tsx": {
|
||||
"modules": 1242,
|
||||
"modules": 1263,
|
||||
"gateways": {
|
||||
"apps/sim/app/workspace/providers/socket-provider.tsx": 1232,
|
||||
"apps/sim/triggers/registry.ts": 487,
|
||||
"apps/sim/app/workspace/providers/socket-provider.tsx": 1253,
|
||||
"apps/sim/triggers/registry.ts": 508,
|
||||
"apps/sim/blocks/registry.ts": 350,
|
||||
"apps/sim/blocks/registry-maps.ts": 347,
|
||||
"apps/sim/stores/workflows/registry/store.ts": 275,
|
||||
@@ -366,10 +366,10 @@
|
||||
}
|
||||
},
|
||||
"app/workspace/page.tsx": {
|
||||
"modules": 1237,
|
||||
"modules": 1258,
|
||||
"gateways": {
|
||||
"apps/sim/lib/auth/stale-session-recovery.ts": 1000,
|
||||
"apps/sim/triggers/index.ts": 488,
|
||||
"apps/sim/lib/auth/stale-session-recovery.ts": 1021,
|
||||
"apps/sim/triggers/index.ts": 509,
|
||||
"apps/sim/blocks/registry.ts": 350,
|
||||
"apps/sim/blocks/registry-maps.ts": 347,
|
||||
"apps/sim/lib/api/contracts/index.ts": 139,
|
||||
|
||||
Reference in New Issue
Block a user