From 3fe0ad512b777b87a58b48ba74c0547250426f7d Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:56:22 -0500 Subject: [PATCH 01/16] azure devops logo on white --- apps/docs/components/icons.tsx | 30 +++++++++++++++++++++++++ apps/docs/components/ui/icon-mapping.ts | 2 ++ 2 files changed, 32 insertions(+) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 9be42a362eb..a54d71fb384 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3048,6 +3048,36 @@ export function AzureIcon(props: SVGProps) { ) } +export function AzureDevOpsIcon(props: SVGProps) { + const id = useId() + const gradientId = `azure_devops_gradient_${id}` + return ( + + + + + + + + + + + + + ) + } + export const GroqIcon = (props: SVGProps) => ( = { ashby: AshbyIcon, athena: AthenaIcon, attio: AttioIcon, + azure_devops: AzureDevOpsIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brightdata: BrightDataIcon, From e20f1a8913076c6cdafd9d22e9e0f5cdefa3c858 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:58:35 -0500 Subject: [PATCH 02/16] generated ADO tool docs --- .../content/docs/en/tools/azure_devops.mdx | 537 ++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/azure_devops.mdx diff --git a/apps/docs/content/docs/en/tools/azure_devops.mdx b/apps/docs/content/docs/en/tools/azure_devops.mdx new file mode 100644 index 00000000000..29bac84e6a5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/azure_devops.mdx @@ -0,0 +1,537 @@ +--- +title: Azure DevOps +description: Interact with Azure DevOps pipelines, builds, and work items +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments. + + + +## Tools + +### `azure_devops_list_pipelines` + +List all pipelines in an Azure DevOps project. Returns pipeline ID, name, folder, revision, and URL. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `orderBy` | string | No | Field to sort results by \(e.g. "name"\) | +| `top` | number | No | Maximum number of pipelines to return | +| `continuationToken` | string | No | Continuation token for paginating results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of pipelines | +| `metadata` | object | Pipelines metadata | +| ↳ `count` | number | Total number of pipelines returned | +| ↳ `pipelines` | array | Array of pipeline objects | +| ↳ `id` | number | Pipeline ID | +| ↳ `name` | string | Pipeline name | +| ↳ `folder` | string | Folder path \(e.g. "\\\\"\) | +| ↳ `revision` | number | Pipeline revision number | +| ↳ `url` | string | Pipeline API URL | + +### `azure_devops_get_pipeline` + +Get details for a specific pipeline in an Azure DevOps project, including configuration and repository info. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `pipelineId` | number | Yes | ID of the pipeline to retrieve | +| `pipelineVersion` | number | No | Specific revision of the pipeline to retrieve \(defaults to latest\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the pipeline | +| `metadata` | object | Pipeline detail metadata | +| ↳ `pipeline` | object | Full pipeline detail object | +| ↳ `id` | number | Pipeline ID | +| ↳ `name` | string | Pipeline name | +| ↳ `folder` | string | Folder path | +| ↳ `revision` | number | Pipeline revision number | +| ↳ `url` | string | Pipeline API URL | +| ↳ `configuration` | object | Pipeline configuration | +| ↳ `type` | string | Configuration type \(e.g. "yaml"\) | +| ↳ `path` | string | YAML file path in the repository | +| ↳ `repository` | object | Source repository info | +| ↳ `id` | string | Repository ID | +| ↳ `type` | string | Repository type \(e.g. "azureReposGit"\) | +| ↳ `links` | object | Hypermedia links | +| ↳ `self` | string | API self-link | +| ↳ `web` | string | Browser URL for the pipeline | + +### `azure_devops_list_pipeline_runs` + +List runs for a specific pipeline in an Azure DevOps project. Returns run ID, name, state, result, and timestamps. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `pipelineId` | number | Yes | ID of the pipeline whose runs to list | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of pipeline runs | +| `metadata` | object | Pipeline runs metadata | +| ↳ `count` | number | Total number of runs returned | +| ↳ `runs` | array | Array of pipeline run objects | +| ↳ `id` | number | Run ID | +| ↳ `name` | string | Run name \(e.g. "20210601.1"\) | +| ↳ `state` | string | Run state \(e.g. "completed", "inProgress"\) | +| ↳ `result` | string | Run result \(e.g. "succeeded", "failed"\) — absent if still running | +| ↳ `createdDate` | string | ISO 8601 creation timestamp | +| ↳ `finishedDate` | string | ISO 8601 finish timestamp — absent if still running | +| ↳ `url` | string | Run API URL | +| ↳ `webUrl` | string | Browser URL for the run | + +### `azure_devops_get_pipeline_run` + +Get details for a specific pipeline run in an Azure DevOps project. Returns run state, result, timestamps, and the pipeline reference. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `pipelineId` | number | Yes | ID of the pipeline | +| `runId` | number | Yes | ID of the run to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the pipeline run | +| `metadata` | object | Pipeline run metadata | +| ↳ `run` | object | Full pipeline run detail object | +| ↳ `id` | number | Run ID | +| ↳ `name` | string | Run name \(e.g. "20210601.1"\) | +| ↳ `state` | string | Run state \(e.g. "completed", "inProgress"\) | +| ↳ `result` | string | Run result \(e.g. "succeeded", "failed"\) — absent if still running | +| ↳ `createdDate` | string | ISO 8601 creation timestamp | +| ↳ `finishedDate` | string | ISO 8601 finish timestamp — absent if still running | +| ↳ `url` | string | Run API URL | +| ↳ `webUrl` | string | Browser URL for the run | +| ↳ `pipeline` | object | Pipeline reference | +| ↳ `id` | number | Pipeline ID | +| ↳ `name` | string | Pipeline name | +| ↳ `folder` | string | Pipeline folder | +| ↳ `revision` | number | Pipeline revision number | +| ↳ `url` | string | Pipeline API URL | + +### `azure_devops_list_builds` + +List builds in an Azure DevOps project. Optionally filter by pipeline definition, status, result, or branch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `definitionIds` | string | No | Comma-separated pipeline definition IDs to filter by \(e.g. "1,2,3"\) | +| `top` | number | No | Maximum number of builds to return | +| `statusFilter` | string | No | Filter by build status: inProgress, completed, cancelling, postponed, notStarted, none | +| `resultFilter` | string | No | Filter by build result: succeeded, partiallySucceeded, failed, canceled | +| `branchName` | string | No | Filter by source branch name \(e.g. "refs/heads/main"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of builds | +| `metadata` | object | Builds metadata | +| ↳ `count` | number | Total number of builds returned | +| ↳ `builds` | array | Array of build objects | +| ↳ `id` | number | Build ID | +| ↳ `buildNumber` | string | Build number \(e.g. "20210601.1"\) | +| ↳ `status` | string | Build status \(e.g. "completed", "inProgress"\) | +| ↳ `result` | string | Build result \(e.g. "succeeded", "failed"\) — absent if still running | +| ↳ `queueTime` | string | ISO 8601 queue timestamp | +| ↳ `startTime` | string | ISO 8601 start timestamp | +| ↳ `finishTime` | string | ISO 8601 finish timestamp — absent if still running | +| ↳ `sourceBranch` | string | Source branch \(e.g. "refs/heads/main"\) | +| ↳ `sourceVersion` | string | Source commit SHA | +| ↳ `definition` | object | Pipeline definition reference | +| ↳ `id` | number | Definition ID | +| ↳ `name` | string | Definition name | +| ↳ `webUrl` | string | Browser URL for the build | + +### `azure_devops_list_build_logs` + +List all log entries for a specific build in Azure DevOps. Returns log IDs, types, and line counts — use the log ID with the Get Build Log tool to fetch actual log text. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `buildId` | number | Yes | The build ID whose logs to list | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of build logs | +| `metadata` | object | Build logs metadata | +| ↳ `count` | number | Total number of log entries returned | +| ↳ `logs` | array | Array of log entry objects | +| ↳ `id` | number | Log entry ID — use with Get Build Log to fetch content | +| ↳ `type` | string | Log type \(e.g. "Container", "Task", "Section"\) | +| ↳ `url` | string | API URL for the log entry | +| ↳ `lineCount` | number | Number of lines in the log | +| ↳ `createdOn` | string | ISO 8601 creation timestamp | +| ↳ `lastChangedOn` | string | ISO 8601 last-changed timestamp | + +### `azure_devops_get_build_log` + +Fetch the text content of a specific build log in Azure DevOps. Use List Build Logs first to get the log ID. Optionally retrieve only a line range with startLine/endLine. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `buildId` | number | Yes | The build ID containing the log | +| `logId` | number | Yes | The log entry ID to fetch \(from List Build Logs\) | +| `startLine` | number | No | First line to return \(1-based, inclusive\) | +| `endLine` | number | No | Last line to return \(1-based, inclusive\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Raw log text | +| `metadata` | object | Log metadata | +| ↳ `lineCount` | number | Number of lines in the returned log text | + +### `azure_devops_get_build_timeline` + +Get the execution timeline for an Azure DevOps build — every stage, job, and task with its result and log ID. Use this to identify which steps failed before fetching their logs with Get Build Log. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `buildId` | number | Yes | ID of the build whose timeline to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Summary of the build timeline, highlighting failed steps | +| `metadata` | object | Build timeline metadata | +| ↳ `totalCount` | number | Total number of timeline records | +| ↳ `failedCount` | number | Number of failed records | +| ↳ `records` | array | All timeline records \(stages, jobs, tasks\) | +| ↳ `id` | string | Record GUID | +| ↳ `name` | string | Step name \(e.g. "Run tests"\) | +| ↳ `type` | string | Stage \| Phase \| Job \| Task | +| ↳ `result` | string | succeeded \| failed \| skipped \| canceled \| null | +| ↳ `logId` | number | Log ID to pass to Get Build Log, or null | +| ↳ `errorCount` | number | Number of errors | +| ↳ `warningCount` | number | Number of warnings | +| ↳ `startTime` | string | ISO 8601 start timestamp | +| ↳ `finishTime` | string | ISO 8601 finish timestamp | +| ↳ `failedRecords` | array | Subset of records where result === "failed" — use logId to fetch logs | +| ↳ `id` | string | Record GUID | +| ↳ `name` | string | Step name | +| ↳ `type` | string | Stage \| Phase \| Job \| Task | +| ↳ `result` | string | failed | +| ↳ `logId` | number | Log ID to pass to Get Build Log | +| ↳ `errorCount` | number | Number of errors | +| ↳ `warningCount` | number | Number of warnings | +| ↳ `startTime` | string | ISO 8601 start timestamp | +| ↳ `finishTime` | string | ISO 8601 finish timestamp | + +### `azure_devops_get_work_items_between_builds` + +Get work item references associated with commits between two builds in Azure DevOps. Returns work item IDs and URLs — use Get Work Items Batch for full field details. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `fromBuildId` | number | Yes | The older build ID \(start of range\) | +| `toBuildId` | number | Yes | The newer build ID \(end of range\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of work items between builds | +| `metadata` | object | Work items metadata | +| ↳ `count` | number | Total number of work item references returned | +| ↳ `workItems` | array | Array of work item references | +| ↳ `id` | string | Work item ID | +| ↳ `url` | string | API URL for the work item | + +### `azure_devops_query_work_items` + +Execute a WIQL query to search for work items in Azure DevOps and return full field details. Use TOP N in your query to limit results (Azure enforces a 200-item maximum per batch fetch). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `wiqlQuery` | string | Yes | WIQL query string \(e.g. "SELECT \[System.Id\] FROM workitems WHERE \[System.State\] = \'Doing\' ORDER BY \[System.Id\] ASC"\). Use TOP N to limit results. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of matching work items | +| `metadata` | object | Work items metadata | +| ↳ `count` | number | Number of work items returned | +| ↳ `workItems` | array | Array of work item details | +| ↳ `id` | number | Work item ID | +| ↳ `title` | string | Work item title | +| ↳ `state` | string | Current state for Basic process \(e.g. To Do, Doing, Done\) | +| ↳ `workItemType` | string | Work item type returned by Azure DevOps \(e.g. Issue, Task, Epic\) | +| ↳ `assignedTo` | string | Display name of assigned user, or null if unassigned | +| ↳ `areaPath` | string | Area path of the work item | +| ↳ `url` | string | API URL for the work item | + +### `azure_devops_get_work_item` + +Fetch full details of a single work item by ID from Azure DevOps, including title, state, type, assignee, and area path. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `workItemId` | number | Yes | The work item ID to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the work item | +| `metadata` | object | Work item metadata | +| ↳ `workItem` | object | Full work item details | +| ↳ `id` | number | Work item ID | +| ↳ `title` | string | Work item title | +| ↳ `state` | string | Current state for Basic process \(e.g. To Do, Doing, Done\) | +| ↳ `workItemType` | string | Work item type returned by Azure DevOps \(e.g. Issue, Task, Epic\) | +| ↳ `assignedTo` | string | Display name of assigned user, or null if unassigned | +| ↳ `areaPath` | string | Area path of the work item | +| ↳ `url` | string | API URL for the work item | + +### `azure_devops_get_work_items_batch` + +Fetch full details for multiple work items by ID from Azure DevOps in a single call. Pass comma-separated IDs (e.g. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `ids` | string | Yes | Comma-separated work item IDs to fetch \(e.g. "123,456,789"\). Maximum 200 IDs. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the fetched work items | +| `metadata` | object | Work items metadata | +| ↳ `count` | number | Number of work items returned | +| ↳ `workItems` | array | Array of work item details | +| ↳ `id` | number | Work item ID | +| ↳ `title` | string | Work item title | +| ↳ `state` | string | Current state for Basic process \(e.g. To Do, Doing, Done\) | +| ↳ `workItemType` | string | Work item type returned by Azure DevOps \(e.g. Issue, Task, Epic\) | +| ↳ `assignedTo` | string | Display name of assigned user, or null if unassigned | +| ↳ `areaPath` | string | Area path of the work item | +| ↳ `url` | string | API URL for the work item | + +### `azure_devops_create_work_item` + +Create a new Basic-process work item (Issue, Task, or Epic) in Azure DevOps. Returns the created work item with its assigned ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `workItemType` | string | Yes | Basic-process work item type to create \("Issue", "Task", or "Epic"\). Use Issue for bug or defect tracking. | +| `title` | string | Yes | Title of the new work item | +| `description` | string | No | HTML description of the work item \(optional\) | +| `assignedTo` | string | No | Email or display name of the user to assign the work item to \(optional\) | +| `priority` | number | No | Priority of the work item \(1 = Critical, 2 = High, 3 = Medium, 4 = Low\) | +| `effort` | number | No | Effort \(Microsoft.VSTS.Scheduling.Effort\). Basic process: Issue only. | +| `startDate` | string | No | Start date \(Microsoft.VSTS.Scheduling.StartDate\), ISO 8601. Basic process: Epic only. | +| `targetDate` | string | No | Target date \(Microsoft.VSTS.Scheduling.TargetDate\), ISO 8601. Basic process: Epic only. | +| `activity` | string | No | Activity \(Microsoft.VSTS.Common.Activity\). One of Deployment, Design, Development, Documentation, Requirements, Testing. Basic process: Task only. | +| `remainingWork` | number | No | Remaining work hours \(Microsoft.VSTS.Scheduling.RemainingWork\). Basic process: Task only. | +| `completedWork` | number | No | Completed work hours \(Microsoft.VSTS.Scheduling.CompletedWork\). Basic process: Task only. | +| `areaPath` | string | No | Area path for the work item, e.g. "MyProject\\\\Team" \(optional\) | +| `iterationPath` | string | No | Iteration path for the work item, e.g. "MyProject\\\\Sprint 1" \(optional\) | +| `tags` | string | No | Semicolon-separated tags, e.g. "issue; p1; auth" \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the created work item | +| `metadata` | object | Created work item metadata | +| ↳ `workItem` | object | Full details of the created work item | +| ↳ `id` | number | Assigned work item ID | +| ↳ `title` | string | Work item title | +| ↳ `state` | string | Initial state for Basic process \(e.g. To Do, Doing, Done\) | +| ↳ `workItemType` | string | Work item type returned by Azure DevOps \(e.g. Issue, Task, Epic\) | +| ↳ `assignedTo` | string | Display name of assigned user, or null if unassigned | +| ↳ `areaPath` | string | Area path of the work item | +| ↳ `url` | string | API URL for the created work item | + +### `azure_devops_update_work_item` + +Update one or more fields on an existing work item in Azure DevOps. Provide only the fields you want to change. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `workItemId` | number | Yes | ID of the work item to update | +| `title` | string | No | New title for the work item \(optional\) | +| `description` | string | No | New HTML description for the work item \(optional\) | +| `assignedTo` | string | No | Email or display name to reassign the work item to \(optional\) | +| `areaPath` | string | No | New area path for the work item \(optional\) | +| `priority` | number | No | Priority of the work item \(1 = Critical, 2 = High, 3 = Medium, 4 = Low\) \(optional\) | +| `state` | string | No | New state for Basic-process work items: "To Do", "Doing", or "Done" \(optional\) | +| `effort` | number | No | Effort \(Microsoft.VSTS.Scheduling.Effort\). Basic process: Issue only. | +| `startDate` | string | No | Start date \(Microsoft.VSTS.Scheduling.StartDate\), ISO 8601. Basic process: Epic only. | +| `targetDate` | string | No | Target date \(Microsoft.VSTS.Scheduling.TargetDate\), ISO 8601. Basic process: Epic only. | +| `activity` | string | No | Activity \(Microsoft.VSTS.Common.Activity\). One of Deployment, Design, Development, Documentation, Requirements, Testing. Basic process: Task only. | +| `remainingWork` | number | No | Remaining work hours \(Microsoft.VSTS.Scheduling.RemainingWork\). Basic process: Task only. | +| `completedWork` | number | No | Completed work hours \(Microsoft.VSTS.Scheduling.CompletedWork\). Basic process: Task only. | +| `tags` | string | No | Semicolon-separated tags to set on the work item \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of the updated work item | +| `metadata` | object | Updated work item metadata | +| ↳ `workItem` | object | Full details of the updated work item | +| ↳ `id` | number | Work item ID | +| ↳ `title` | string | Work item title | +| ↳ `state` | string | Current state after update | +| ↳ `workItemType` | string | Work item type returned by Azure DevOps \(e.g. Issue, Task, Epic\) | +| ↳ `assignedTo` | string | Display name of assigned user, or null if unassigned | +| ↳ `areaPath` | string | Area path of the work item | +| ↳ `url` | string | API URL for the work item | + +### `azure_devops_add_comment` + +Add a comment to a work item in Azure DevOps. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `workItemId` | number | Yes | ID of the work item to comment on | +| `text` | string | Yes | Comment text \(HTML supported, e.g. "<p>My comment</p>"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable confirmation of the added comment | +| `metadata` | object | Added comment metadata | +| ↳ `comment` | object | Full details of the created comment | +| ↳ `workItemId` | number | Work item the comment belongs to | +| ↳ `commentId` | number | Comment ID | +| ↳ `version` | number | Comment version | +| ↳ `text` | string | Comment text | +| ↳ `renderedText` | string | Rendered HTML comment text when available | +| ↳ `createdBy` | string | Display name of the comment author, or null | +| ↳ `createdDate` | string | ISO timestamp when comment was created | +| ↳ `modifiedBy` | string | Display name of the last modifier, or null | +| ↳ `modifiedDate` | string | ISO timestamp when comment was modified | +| ↳ `isDeleted` | boolean | Whether the comment is deleted | +| ↳ `url` | string | API URL for the comment | + +### `azure_devops_get_comments` + +List comments for an Azure DevOps work item. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organization` | string | Yes | Azure DevOps organization name | +| `project` | string | Yes | Azure DevOps project name | +| `workItemId` | number | Yes | ID of the work item whose comments should be listed | +| `top` | number | No | Maximum number of comments to return | +| `continuationToken` | string | No | Continuation token for paginating comments | +| `includeDeleted` | boolean | No | Whether deleted comments should be returned | +| `expand` | string | No | Additional comment data to include: none, reactions, renderedText, renderedTextOnly, all | +| `order` | string | No | Sort order for comments: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Human-readable summary of work item comments | +| `metadata` | object | Comments metadata | +| ↳ `count` | number | Number of comments returned in this page | +| ↳ `totalCount` | number | Total number of comments on the work item | +| ↳ `continuationToken` | string | Continuation token for the next page | +| ↳ `nextPage` | string | API URL for the next page | +| ↳ `url` | string | API URL for this comments list | +| ↳ `comments` | array | Array of work item comments | +| ↳ `workItemId` | number | Work item ID | +| ↳ `commentId` | number | Comment ID | +| ↳ `version` | number | Comment version | +| ↳ `text` | string | Comment text | +| ↳ `renderedText` | string | Rendered HTML comment text when available | +| ↳ `createdBy` | string | Display name of the comment author | +| ↳ `createdDate` | string | ISO 8601 creation timestamp | +| ↳ `modifiedBy` | string | Display name of the last modifier | +| ↳ `modifiedDate` | string | ISO 8601 modified timestamp | +| ↳ `isDeleted` | boolean | Whether the comment is deleted | +| ↳ `url` | string | API URL for the comment | + + From f655dfde1fc8c74f27d5958e760817e2039a2155 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:58:59 -0500 Subject: [PATCH 03/16] generated ADO tool docs --- apps/docs/content/docs/en/tools/meta.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index ad0f6b437ad..82bdb10566e 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -17,6 +17,7 @@ "ashby", "athena", "attio", + "azure_devops", "box", "brandfetch", "brightdata", From 07142d04bc53027ecebed6f5dd27f1c6788c35cb Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:02:50 -0500 Subject: [PATCH 04/16] added ADO to registries --- .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 115 +++++++++++++++++- apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/tools/registry.ts | 34 ++++++ 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index f80e850cebe..adc10783004 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -20,6 +20,7 @@ import { AshbyIcon, AthenaIcon, AttioIcon, + AzureDevOpsIcon, AzureIcon, BoxCompanyIcon, BrainIcon, @@ -223,6 +224,7 @@ export const blockTypeToIconMap: Record = { ashby: AshbyIcon, athena: AthenaIcon, attio: AttioIcon, + azure_devops: AzureDevOpsIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brightdata: BrightDataIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 3e6de1c0eef..8145b71798c 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -1882,6 +1882,105 @@ "integrationTypes": ["security"], "tags": ["identity", "microsoft-365"] }, + { + "type": "azure_devops", + "slug": "azure-devops", + "name": "Azure DevOps", + "description": "Interact with Azure DevOps pipelines, builds, and work items", + "longDescription": "Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.", + "bgColor": "#FFFFFF", + "iconName": "AzureDevOpsIcon", + "docsUrl": "https://docs.sim.ai/tools/azure_devops", + "operations": [ + { + "name": "List Pipelines", + "description": "List all pipelines in an Azure DevOps project. Returns pipeline ID, name, folder, revision, and URL." + }, + { + "name": "Get Pipeline", + "description": "Get details for a specific pipeline in an Azure DevOps project, including configuration and repository info." + }, + { + "name": "List Pipeline Runs", + "description": "List runs for a specific pipeline in an Azure DevOps project. Returns run ID, name, state, result, and timestamps." + }, + { + "name": "Get Pipeline Run", + "description": "Get details for a specific pipeline run in an Azure DevOps project. Returns run state, result, timestamps, and the pipeline reference." + }, + { + "name": "List Builds", + "description": "List builds in an Azure DevOps project. Optionally filter by pipeline definition, status, result, or branch." + }, + { + "name": "List Build Logs", + "description": "List all log entries for a specific build in Azure DevOps. Returns log IDs, types, and line counts — use the log ID with the Get Build Log tool to fetch actual log text." + }, + { + "name": "Get Build Log", + "description": "Fetch the text content of a specific build log in Azure DevOps. Use List Build Logs first to get the log ID. Optionally retrieve only a line range with startLine/endLine." + }, + { + "name": "Get Build Timeline", + "description": "Get the execution timeline for an Azure DevOps build — every stage, job, and task with its result and log ID. Use this to identify which steps failed before fetching their logs with Get Build Log." + }, + { + "name": "Get Work Items Between Builds", + "description": "Get work item references associated with commits between two builds in Azure DevOps. Returns work item IDs and URLs — use Get Work Items Batch for full field details." + }, + { + "name": "Query Work Items", + "description": "Execute a WIQL query to search for work items in Azure DevOps and return full field details. Use TOP N in your query to limit results (Azure enforces a 200-item maximum per batch fetch)." + }, + { + "name": "Get Work Item", + "description": "Fetch full details of a single work item by ID from Azure DevOps, including title, state, type, assignee, and area path." + }, + { + "name": "Get Work Items Batch", + "description": "Fetch full details for multiple work items by ID from Azure DevOps in a single call. Pass comma-separated IDs (e.g. " + }, + { + "name": "Create Work Item", + "description": "Create a new Basic-process work item (Issue, Task, or Epic) in Azure DevOps. Returns the created work item with its assigned ID." + }, + { + "name": "Update Work Item", + "description": "Update one or more fields on an existing work item in Azure DevOps. Provide only the fields you want to change." + }, + { + "name": "Add Comment", + "description": "Add a comment to a work item in Azure DevOps." + }, + { + "name": "Get Comments", + "description": "List comments for an Azure DevOps work item." + } + ], + "operationCount": 16, + "triggers": [ + { + "id": "azure_devops_build_failed", + "name": "Azure DevOps Build Failed", + "description": "Trigger workflow when an Azure DevOps build fails, is canceled, or partially succeeds" + }, + { + "id": "azure_devops_work_item_created", + "name": "Azure DevOps Work Item Created", + "description": "Trigger workflow when a work item is created in Azure DevOps" + }, + { + "id": "azure_devops_webhook", + "name": "Azure DevOps Webhook (All Service Hook Events)", + "description": "Trigger on whichever service hook event types you configure in Azure DevOps. Sim does not filter deliveries for this trigger." + } + ], + "triggerCount": 3, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["developer-tools", "productivity"], + "tags": ["ci-cd", "project-management", "version-control"] + }, { "type": "box", "slug": "box", @@ -2540,9 +2639,17 @@ { "name": "Describe Alarms", "description": "List and filter CloudWatch alarms" + }, + { + "name": "Mute Alarm", + "description": "Disable notification actions on one or more CloudWatch alarms" + }, + { + "name": "Unmute Alarm", + "description": "Re-enable notification actions on one or more CloudWatch alarms" } ], - "operationCount": 8, + "operationCount": 10, "triggers": [], "triggerCount": 0, "authType": "none", @@ -4045,6 +4152,10 @@ "name": "Read", "description": "Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)" }, + { + "name": "Get", + "description": "Get a workspace file object from a selected file or canonical workspace file ID." + }, { "name": "Write", "description": "Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g., " @@ -4054,7 +4165,7 @@ "description": "Append content to an existing workspace file. The file must already exist. Content is added to the end of the file." } ], - "operationCount": 3, + "operationCount": 4, "triggers": [], "triggerCount": 0, "authType": "none", diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index ce8e9c6af73..5c4d8eaea73 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -3,6 +3,7 @@ import { NextResponse } from 'next/server' 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 { calcomHandler } from '@/lib/webhooks/providers/calcom' import { calendlyHandler } from '@/lib/webhooks/providers/calendly' import { circlebackHandler } from '@/lib/webhooks/providers/circleback' @@ -52,6 +53,7 @@ const PROVIDER_HANDLERS: Record = { airtable: airtableHandler, ashby: ashbyHandler, attio: attioHandler, + azure_devops: azureDevOpsHandler, calendly: calendlyHandler, calcom: calcomHandler, circleback: circlebackHandler, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index c71385aa0d9..4818ff8dab0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -241,6 +241,24 @@ import { attioUpdateTaskTool, attioUpdateWebhookTool, } from '@/tools/attio' +import { + getBuildLogTool as azureDevopsGetBuildLogTool, + getBuildTimelineTool as azureDevopsGetBuildTimelineTool, + getCommentsTool as azureDevopsGetCommentsTool, + getPipelineRunTool as azureDevopsGetPipelineRunTool, + getWorkItemsBatchTool as azureDevopsGetWorkItemsBatchTool, + getWorkItemsBetweenBuildsTool as azureDevopsGetWorkItemsBetweenBuildsTool, + getWorkItemTool as azureDevopsGetWorkItemTool, + getPipelineTool as azureDevopsGetPipelineTool, + listBuildLogsTool as azureDevopsListBuildLogsTool, + listBuildsTool as azureDevopsListBuildsTool, + listPipelineRunsTool as azureDevopsListPipelineRunsTool, + listPipelinesTool as azureDevopsListPipelinesTool, + queryWorkItemsTool as azureDevopsQueryWorkItemsTool, + createWorkItemTool as azureDevopsCreateWorkItemTool, + updateWorkItemTool as azureDevopsUpdateWorkItemTool, + addCommentTool as azureDevopsAddCommentTool, +} from '@/tools/azure_devops' import { boxCopyFileTool, boxCreateFolderTool, @@ -3183,6 +3201,22 @@ export const tools: Record = { brightdata_serp_search: brightDataSerpSearchTool, brightdata_snapshot_status: brightDataSnapshotStatusTool, brightdata_sync_scrape: brightDataSyncScrapeTool, + azure_devops_list_pipelines: azureDevopsListPipelinesTool, + azure_devops_get_pipeline: azureDevopsGetPipelineTool, + azure_devops_list_pipeline_runs: azureDevopsListPipelineRunsTool, + azure_devops_get_pipeline_run: azureDevopsGetPipelineRunTool, + azure_devops_list_builds: azureDevopsListBuildsTool, + azure_devops_list_build_logs: azureDevopsListBuildLogsTool, + azure_devops_get_build_log: azureDevopsGetBuildLogTool, + azure_devops_get_build_timeline: azureDevopsGetBuildTimelineTool, + azure_devops_get_work_items_between_builds: azureDevopsGetWorkItemsBetweenBuildsTool, + azure_devops_query_work_items: azureDevopsQueryWorkItemsTool, + azure_devops_get_work_item: azureDevopsGetWorkItemTool, + azure_devops_get_work_items_batch: azureDevopsGetWorkItemsBatchTool, + azure_devops_create_work_item: azureDevopsCreateWorkItemTool, + azure_devops_update_work_item: azureDevopsUpdateWorkItemTool, + azure_devops_add_comment: azureDevopsAddCommentTool, + azure_devops_get_comments: azureDevopsGetCommentsTool, box_copy_file: boxCopyFileTool, box_create_folder: boxCreateFolderTool, box_delete_file: boxDeleteFileTool, From d28fe0417c3e5aa968b38342e04a25247cfd9859 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:04:43 -0500 Subject: [PATCH 05/16] ADO workflow triggers --- .../sim/triggers/azure_devops/build_failed.ts | 34 +++ apps/sim/triggers/azure_devops/index.ts | 3 + apps/sim/triggers/azure_devops/utils.ts | 280 ++++++++++++++++++ apps/sim/triggers/azure_devops/webhook.ts | 37 +++ .../azure_devops/work_item_created.ts | 32 ++ 5 files changed, 386 insertions(+) create mode 100644 apps/sim/triggers/azure_devops/build_failed.ts create mode 100644 apps/sim/triggers/azure_devops/index.ts create mode 100644 apps/sim/triggers/azure_devops/utils.ts create mode 100644 apps/sim/triggers/azure_devops/webhook.ts create mode 100644 apps/sim/triggers/azure_devops/work_item_created.ts diff --git a/apps/sim/triggers/azure_devops/build_failed.ts b/apps/sim/triggers/azure_devops/build_failed.ts new file mode 100644 index 00000000000..acd01f51754 --- /dev/null +++ b/apps/sim/triggers/azure_devops/build_failed.ts @@ -0,0 +1,34 @@ +import { AzureDevOpsIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + azureDevOpsTriggerOptions, + buildBuildFailedOutputs, + buildFailedSetupInstructions, +} from '@/triggers/azure_devops/utils' + +export const azureDevOpsBuildFailedTrigger: TriggerConfig = { + id: 'azure_devops_build_failed', + name: 'Azure DevOps Build Failed', + provider: 'azure_devops', + description: + 'Trigger workflow when an Azure DevOps build fails, is canceled, or partially succeeds', + version: '1.0.0', + icon: AzureDevOpsIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'azure_devops_build_failed', + triggerOptions: azureDevOpsTriggerOptions, + includeDropdown: true, + setupInstructions: buildFailedSetupInstructions, + }), + + outputs: buildBuildFailedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/azure_devops/index.ts b/apps/sim/triggers/azure_devops/index.ts new file mode 100644 index 00000000000..5ef419ea36d --- /dev/null +++ b/apps/sim/triggers/azure_devops/index.ts @@ -0,0 +1,3 @@ +export { azureDevOpsBuildFailedTrigger } from './build_failed' +export { azureDevOpsWebhookTrigger } from './webhook' +export { azureDevOpsWorkItemCreatedTrigger } from './work_item_created' diff --git a/apps/sim/triggers/azure_devops/utils.ts b/apps/sim/triggers/azure_devops/utils.ts new file mode 100644 index 00000000000..9c44c6224e5 --- /dev/null +++ b/apps/sim/triggers/azure_devops/utils.ts @@ -0,0 +1,280 @@ +import type { TriggerOutput } from '@/triggers/types' + +export const azureDevOpsTriggerOptions = [ + { label: 'Build Failed', id: 'azure_devops_build_failed' }, + { label: 'Work Item Created', id: 'azure_devops_work_item_created' }, + { label: 'All Service Hook Events', id: 'azure_devops_webhook' }, +] + +export const AZURE_DEVOPS_BUILD_FAILED_EVENT = 'build.complete' +export const AZURE_DEVOPS_WORK_ITEM_CREATED_EVENT = 'workitem.created' + +function instructions(steps: string[]): string { + return steps + .map((s, i) => `
${i + 1}. ${s}
`) + .join('') +} + +export const buildFailedSetupInstructions = instructions([ + 'Open your Azure DevOps project and go to Project settings → Service hooks.', + 'Click + Create subscription, choose Web Hooks, then Next.', + 'For Trigger on this type of event, select Build completed.', + 'Under Filters, set Build result to Failed (optionally add Canceled / Partially succeeded).', + 'Click Next, paste the Webhook URL above into the URL field.', + 'Leave other fields as defaults. Click Test to verify, then Finish.', +]) + +export const workItemCreatedSetupInstructions = instructions([ + 'Open your Azure DevOps project and go to Project settings → Service hooks.', + 'Click + Create subscription, choose Web Hooks, then Next.', + 'For Trigger on this type of event, select Work item created.', + 'Click Next, paste the Webhook URL above into the URL field.', + 'Leave other fields as defaults. Click Test to verify, then Finish.', +]) + +export const webhookSetupInstructions = instructions([ + 'Open your Azure DevOps project and go to Project settings → Service hooks.', + 'Click + Create subscription, choose Web Hooks, then Next.', + 'Select whichever event types you want this URL to receive (build, work item, release, etc.).', + 'Click Next, paste the Webhook URL above into the URL field.', + 'Leave other fields as defaults. Click Test to verify, then Finish.', + 'Sim does not filter deliveries for this trigger — configure event types in Azure DevOps.', +]) + +/** + * Returns whether an Azure DevOps service hook payload matches the configured trigger. + */ +export function isAzureDevOpsEventMatch( + triggerId: string, + body: Record +): boolean { + if (triggerId === 'azure_devops_webhook') { + return true + } + + const eventType = body.eventType as string | undefined + + if (triggerId === 'azure_devops_build_failed') { + if (eventType !== AZURE_DEVOPS_BUILD_FAILED_EVENT) { + return false + } + const resource = body.resource as Record | undefined + const result = resource?.result as string | undefined + return result !== 'succeeded' + } + + if (triggerId === 'azure_devops_work_item_created') { + return eventType === AZURE_DEVOPS_WORK_ITEM_CREATED_EVENT + } + + return false +} + +export function buildBuildFailedOutputs(): Record { + return { + buildId: { + type: 'number', + description: 'Build ID', + }, + buildNumber: { + type: 'string', + description: 'Build number string (e.g. 20240101.1)', + }, + result: { + type: 'string', + description: 'Build result: failed | canceled | partiallySucceeded', + }, + pipelineId: { + type: 'number', + description: 'Pipeline definition ID', + }, + pipelineName: { + type: 'string', + description: 'Pipeline definition name', + }, + projectName: { + type: 'string', + description: 'Azure DevOps project name', + }, + branch: { + type: 'string', + description: 'Source branch name (refs/heads/ prefix stripped)', + }, + commitSha: { + type: 'string', + description: 'Source commit SHA', + }, + triggeredBy: { + type: 'string', + description: 'Display name of the person who triggered the build', + }, + triggeredByEmail: { + type: 'string', + description: 'Email/unique name of the person who triggered the build', + }, + startTime: { + type: 'string', + description: 'Build start time (ISO 8601)', + }, + finishTime: { + type: 'string', + description: 'Build finish time (ISO 8601)', + }, + buildUrl: { + type: 'string', + description: 'API URL for the build resource', + }, + } +} + +export function buildWorkItemCreatedOutputs(): Record { + return { + workItemId: { + type: 'number', + description: 'Work item ID', + }, + workItemType: { + type: 'string', + description: 'Work item type for Basic process (e.g. Issue, Task, Epic)', + }, + title: { + type: 'string', + description: 'Work item title', + }, + state: { + type: 'string', + description: 'Work item state for Basic process (e.g. To Do, Doing, Done)', + }, + createdBy: { + type: 'string', + description: 'Display name of the creator', + }, + assignedTo: { + type: 'string', + description: 'Assignee display name, or empty string if unassigned', + }, + priority: { + type: 'number', + description: 'Priority (1–4), or 0 if not set', + }, + areaPath: { + type: 'string', + description: 'Area path', + }, + iterationPath: { + type: 'string', + description: 'Iteration path', + }, + description: { + type: 'string', + description: 'Work item description (HTML), or empty string if not set', + }, + projectName: { + type: 'string', + description: 'Azure DevOps project name', + }, + workItemUrl: { + type: 'string', + description: 'API URL for the work item resource', + }, + } +} + +export function buildWebhookOutputs(): Record { + return { + eventType: { + type: 'string', + description: 'Service hook event type (e.g. build.complete, workitem.created)', + }, + notificationId: { + type: 'number', + description: 'Notification ID', + }, + subscriptionId: { + type: 'string', + description: 'Service hook subscription ID', + }, + publisherId: { + type: 'string', + description: 'Publisher ID (e.g. tfs)', + }, + createdDate: { + type: 'string', + description: 'Event creation time (ISO 8601)', + }, + resource: { + type: 'json', + description: 'Event resource payload', + }, + resourceContainers: { + type: 'json', + description: 'Resource container references (project, collection, etc.)', + }, + message: { + type: 'json', + description: 'Short message object', + }, + detailedMessage: { + type: 'json', + description: 'Detailed message object', + }, + } +} + +export function formatBuildCompleteInput(body: Record): Record { + const resource = (body.resource ?? {}) as Record + const definition = (resource.definition ?? {}) as Record + const project = (resource.project ?? {}) as Record + const requestedFor = (resource.requestedFor ?? {}) as Record + const sourceBranch = (resource.sourceBranch as string) ?? '' + + return { + buildId: Number(resource.id ?? 0), + buildNumber: (resource.buildNumber as string) ?? '', + result: (resource.result as string) ?? '', + pipelineId: Number(definition.id ?? 0), + pipelineName: (definition.name as string) ?? '', + projectName: (project.name as string) ?? '', + branch: sourceBranch.replace(/^refs\/heads\//, ''), + commitSha: (resource.sourceVersion as string) ?? '', + triggeredBy: (requestedFor.displayName as string) ?? '', + triggeredByEmail: (requestedFor.uniqueName as string) ?? '', + startTime: (resource.startTime as string) ?? '', + finishTime: (resource.finishTime as string) ?? '', + buildUrl: (resource.url as string) ?? '', + } +} + +export function formatWorkItemCreatedInput(body: Record): Record { + const resource = (body.resource ?? {}) as Record + const fields = (resource.fields ?? {}) as Record + + return { + workItemId: Number(resource.id ?? 0), + workItemType: (fields['System.WorkItemType'] as string) ?? '', + title: (fields['System.Title'] as string) ?? '', + state: (fields['System.State'] as string) ?? '', + createdBy: (fields['System.CreatedBy'] as string) ?? '', + assignedTo: (fields['System.AssignedTo'] as string) ?? '', + priority: Number(fields['Microsoft.VSTS.Common.Priority'] ?? 0), + areaPath: (fields['System.AreaPath'] as string) ?? '', + iterationPath: (fields['System.IterationPath'] as string) ?? '', + description: (fields['System.Description'] as string) ?? '', + projectName: (fields['System.TeamProject'] as string) ?? '', + workItemUrl: (resource.url as string) ?? '', + } +} + +export function formatWebhookEnvelopeInput(body: Record): Record { + return { + eventType: (body.eventType as string) ?? '', + notificationId: Number(body.notificationId ?? 0), + subscriptionId: (body.subscriptionId as string) ?? '', + publisherId: (body.publisherId as string) ?? '', + createdDate: (body.createdDate as string) ?? '', + resource: body.resource ?? null, + resourceContainers: body.resourceContainers ?? null, + message: body.message ?? null, + detailedMessage: body.detailedMessage ?? null, + } +} diff --git a/apps/sim/triggers/azure_devops/webhook.ts b/apps/sim/triggers/azure_devops/webhook.ts new file mode 100644 index 00000000000..fda04424090 --- /dev/null +++ b/apps/sim/triggers/azure_devops/webhook.ts @@ -0,0 +1,37 @@ +import { AzureDevOpsIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + azureDevOpsTriggerOptions, + buildWebhookOutputs, + webhookSetupInstructions, +} from '@/triggers/azure_devops/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Azure DevOps generic webhook trigger. + * Event filtering is determined by which events you enable on the service hook subscription. + */ +export const azureDevOpsWebhookTrigger: TriggerConfig = { + id: 'azure_devops_webhook', + name: 'Azure DevOps Webhook (All Service Hook Events)', + provider: 'azure_devops', + description: + 'Trigger on whichever service hook event types you configure in Azure DevOps. Sim does not filter deliveries for this trigger.', + version: '1.0.0', + icon: AzureDevOpsIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'azure_devops_webhook', + triggerOptions: azureDevOpsTriggerOptions, + setupInstructions: webhookSetupInstructions, + }), + + outputs: buildWebhookOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/azure_devops/work_item_created.ts b/apps/sim/triggers/azure_devops/work_item_created.ts new file mode 100644 index 00000000000..752485add7c --- /dev/null +++ b/apps/sim/triggers/azure_devops/work_item_created.ts @@ -0,0 +1,32 @@ +import { AzureDevOpsIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + azureDevOpsTriggerOptions, + buildWorkItemCreatedOutputs, + workItemCreatedSetupInstructions, +} from '@/triggers/azure_devops/utils' + +export const azureDevOpsWorkItemCreatedTrigger: TriggerConfig = { + id: 'azure_devops_work_item_created', + name: 'Azure DevOps Work Item Created', + provider: 'azure_devops', + description: 'Trigger workflow when a work item is created in Azure DevOps', + version: '1.0.0', + icon: AzureDevOpsIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'azure_devops_work_item_created', + triggerOptions: azureDevOpsTriggerOptions, + setupInstructions: workItemCreatedSetupInstructions, + }), + + outputs: buildWorkItemCreatedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} From e0ebb423a76d249bc69fb8f794bb174796960661 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:05:09 -0500 Subject: [PATCH 06/16] ADO workflow triggers --- apps/sim/triggers/registry.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index b1d747f529f..e458c36b64f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -7,6 +7,11 @@ import { ashbyJobCreateTrigger, ashbyOfferCreateTrigger, } from '@/triggers/ashby' +import { + azureDevOpsBuildFailedTrigger, + azureDevOpsWebhookTrigger, + azureDevOpsWorkItemCreatedTrigger, +} from '@/triggers/azure_devops' import { attioCommentCreatedTrigger, attioCommentDeletedTrigger, @@ -340,6 +345,9 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { ashby_candidate_delete: ashbyCandidateDeleteTrigger, ashby_job_create: ashbyJobCreateTrigger, ashby_offer_create: ashbyOfferCreateTrigger, + azure_devops_build_failed: azureDevOpsBuildFailedTrigger, + azure_devops_webhook: azureDevOpsWebhookTrigger, + azure_devops_work_item_created: azureDevOpsWorkItemCreatedTrigger, attio_webhook: attioWebhookTrigger, attio_record_created: attioRecordCreatedTrigger, attio_record_updated: attioRecordUpdatedTrigger, From 5de595f0e125e38bec276994c3b6efff109cfd07 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:06:35 -0500 Subject: [PATCH 07/16] tool layer for ADO, checks passed and manual verified --- apps/sim/tools/azure_devops/add_comment.ts | 122 +++++ .../tools/azure_devops/create_work_item.ts | 236 +++++++++ apps/sim/tools/azure_devops/get_build_log.ts | 101 ++++ .../tools/azure_devops/get_build_timeline.ts | 158 ++++++ apps/sim/tools/azure_devops/get_comments.ts | 186 +++++++ apps/sim/tools/azure_devops/get_pipeline.ts | 167 +++++++ .../tools/azure_devops/get_pipeline_run.ts | 157 ++++++ apps/sim/tools/azure_devops/get_work_item.ts | 103 ++++ .../azure_devops/get_work_items_batch.ts | 121 +++++ .../get_work_items_between_builds.ts | 126 +++++ apps/sim/tools/azure_devops/index.ts | 35 ++ .../sim/tools/azure_devops/list_build_logs.ts | 134 +++++ apps/sim/tools/azure_devops/list_builds.ts | 203 ++++++++ .../tools/azure_devops/list_pipeline_runs.ts | 149 ++++++ apps/sim/tools/azure_devops/list_pipelines.ts | 133 +++++ .../tools/azure_devops/query_work_items.ts | 147 ++++++ apps/sim/tools/azure_devops/types.ts | 456 ++++++++++++++++++ .../tools/azure_devops/update_work_item.ts | 247 ++++++++++ apps/sim/tools/azure_devops/utils.ts | 120 +++++ 19 files changed, 3101 insertions(+) create mode 100644 apps/sim/tools/azure_devops/add_comment.ts create mode 100644 apps/sim/tools/azure_devops/create_work_item.ts create mode 100644 apps/sim/tools/azure_devops/get_build_log.ts create mode 100644 apps/sim/tools/azure_devops/get_build_timeline.ts create mode 100644 apps/sim/tools/azure_devops/get_comments.ts create mode 100644 apps/sim/tools/azure_devops/get_pipeline.ts create mode 100644 apps/sim/tools/azure_devops/get_pipeline_run.ts create mode 100644 apps/sim/tools/azure_devops/get_work_item.ts create mode 100644 apps/sim/tools/azure_devops/get_work_items_batch.ts create mode 100644 apps/sim/tools/azure_devops/get_work_items_between_builds.ts create mode 100644 apps/sim/tools/azure_devops/index.ts create mode 100644 apps/sim/tools/azure_devops/list_build_logs.ts create mode 100644 apps/sim/tools/azure_devops/list_builds.ts create mode 100644 apps/sim/tools/azure_devops/list_pipeline_runs.ts create mode 100644 apps/sim/tools/azure_devops/list_pipelines.ts create mode 100644 apps/sim/tools/azure_devops/query_work_items.ts create mode 100644 apps/sim/tools/azure_devops/types.ts create mode 100644 apps/sim/tools/azure_devops/update_work_item.ts create mode 100644 apps/sim/tools/azure_devops/utils.ts diff --git a/apps/sim/tools/azure_devops/add_comment.ts b/apps/sim/tools/azure_devops/add_comment.ts new file mode 100644 index 00000000000..bc731aca7aa --- /dev/null +++ b/apps/sim/tools/azure_devops/add_comment.ts @@ -0,0 +1,122 @@ +import type { + AddCommentParams, + AddCommentResponse, + AzureDevOpsComment, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsRawComment } from '@/tools/azure_devops/utils' +import { formatComment, mapComment } from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const addCommentTool: ToolConfig = { + id: 'azure_devops_add_comment', + name: 'Azure DevOps Add Comment', + description: 'Add a comment to a work item in Azure DevOps.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + workItemId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the work item to comment on', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment text (HTML supported, e.g. "

My comment

")', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read & Write)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workItems/${Number(params.workItemId)}/comments` + ) + url.searchParams.set('api-version', '7.2-preview.4') + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + body: (params) => ({ text: params.text }), + }, + + transformResponse: async (response) => { + const raw: AzureDevOpsRawComment = await response.json() + const comment: AzureDevOpsComment = mapComment(raw) + + return { + success: true, + output: { + content: `Added comment #${comment.commentId}:\n\n${formatComment(comment)}`, + metadata: { comment }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable confirmation of the added comment', + }, + metadata: { + type: 'object', + description: 'Added comment metadata', + properties: { + comment: { + type: 'object', + description: 'Full details of the created comment', + properties: { + workItemId: { type: 'number', description: 'Work item the comment belongs to' }, + commentId: { type: 'number', description: 'Comment ID' }, + version: { type: 'number', description: 'Comment version' }, + text: { type: 'string', description: 'Comment text' }, + renderedText: { + type: 'string', + description: 'Rendered HTML comment text when available', + optional: true, + }, + createdBy: { + type: 'string', + description: 'Display name of the comment author, or null', + nullable: true, + }, + createdDate: { type: 'string', description: 'ISO timestamp when comment was created' }, + modifiedBy: { + type: 'string', + description: 'Display name of the last modifier, or null', + nullable: true, + }, + modifiedDate: { + type: 'string', + description: 'ISO timestamp when comment was modified', + }, + isDeleted: { type: 'boolean', description: 'Whether the comment is deleted' }, + url: { type: 'string', description: 'API URL for the comment' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/create_work_item.ts b/apps/sim/tools/azure_devops/create_work_item.ts new file mode 100644 index 00000000000..73c721ad6a0 --- /dev/null +++ b/apps/sim/tools/azure_devops/create_work_item.ts @@ -0,0 +1,236 @@ +import type { + AzureDevOpsWorkItem, + CreateWorkItemParams, + CreateWorkItemResponse, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsJsonPatchOp, AzureDevOpsRawWorkItem } from '@/tools/azure_devops/utils' +import { + appendEffortPatchOp, + appendFieldPatchOp, + formatWorkItem, + mapWorkItem, +} from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const createWorkItemTool: ToolConfig = { + id: 'azure_devops_create_work_item', + name: 'Azure DevOps Create Work Item', + description: + 'Create a new Basic-process work item (Issue, Task, or Epic) in Azure DevOps. Returns the created work item with its assigned ID.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + workItemType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Basic-process work item type to create ("Issue", "Task", or "Epic"). Use Issue for bug or defect tracking.', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Title of the new work item', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'HTML description of the work item (optional)', + }, + assignedTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email or display name of the user to assign the work item to (optional)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Priority of the work item (1 = Critical, 2 = High, 3 = Medium, 4 = Low)', + }, + effort: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Effort (Microsoft.VSTS.Scheduling.Effort). Basic process: Issue only.', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Start date (Microsoft.VSTS.Scheduling.StartDate), ISO 8601. Basic process: Epic only.', + }, + targetDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Target date (Microsoft.VSTS.Scheduling.TargetDate), ISO 8601. Basic process: Epic only.', + }, + activity: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Activity (Microsoft.VSTS.Common.Activity). One of Deployment, Design, Development, Documentation, Requirements, Testing. Basic process: Task only.', + }, + remainingWork: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Remaining work hours (Microsoft.VSTS.Scheduling.RemainingWork). Basic process: Task only.', + }, + completedWork: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Completed work hours (Microsoft.VSTS.Scheduling.CompletedWork). Basic process: Task only.', + }, + areaPath: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Area path for the work item, e.g. "MyProject\\\\Team" (optional)', + }, + iterationPath: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Iteration path for the work item, e.g. "MyProject\\\\Sprint 1" (optional)', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated tags, e.g. "issue; p1; auth" (optional)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read & Write)', + }, + }, + + request: { + url: (params) => + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/$${encodeURIComponent(params.workItemType)}?api-version=7.2-preview.3`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json-patch+json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + body: (params) => { + const ops: AzureDevOpsJsonPatchOp[] = [ + { op: 'add', path: '/fields/System.Title', value: params.title }, + ] + if (params.description) { + ops.push({ op: 'add', path: '/fields/System.Description', value: params.description }) + } + if (params.assignedTo) { + ops.push({ op: 'add', path: '/fields/System.AssignedTo', value: params.assignedTo }) + } + if (params.priority !== undefined) { + ops.push({ + op: 'add', + path: '/fields/Microsoft.VSTS.Common.Priority', + value: String(Number(params.priority)), + }) + } + appendEffortPatchOp(ops, params.effort, 'add') + appendFieldPatchOp(ops, 'Microsoft.VSTS.Scheduling.StartDate', params.startDate, 'add', 'string') + appendFieldPatchOp(ops, 'Microsoft.VSTS.Scheduling.TargetDate', params.targetDate, 'add', 'string') + appendFieldPatchOp(ops, 'Microsoft.VSTS.Common.Activity', params.activity, 'add', 'string') + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.RemainingWork', + params.remainingWork, + 'add', + 'number' + ) + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.CompletedWork', + params.completedWork, + 'add', + 'number' + ) + if (params.areaPath) { + ops.push({ op: 'add', path: '/fields/System.AreaPath', value: params.areaPath }) + } + if (params.iterationPath) { + ops.push({ op: 'add', path: '/fields/System.IterationPath', value: params.iterationPath }) + } + if (params.tags) { + ops.push({ op: 'add', path: '/fields/System.Tags', value: params.tags }) + } + return ops + }, + }, + + transformResponse: async (response) => { + const raw: AzureDevOpsRawWorkItem = await response.json() + const workItem: AzureDevOpsWorkItem = mapWorkItem(raw) + return { + success: true, + output: { + content: `Created work item #${workItem.id}:\n\n${formatWorkItem(workItem)}`, + metadata: { workItem }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of the created work item', + }, + metadata: { + type: 'object', + description: 'Created work item metadata', + properties: { + workItem: { + type: 'object', + description: 'Full details of the created work item', + properties: { + id: { type: 'number', description: 'Assigned work item ID' }, + title: { type: 'string', description: 'Work item title' }, + state: { + type: 'string', + description: 'Initial state for Basic process (e.g. To Do, Doing, Done)', + }, + workItemType: { + type: 'string', + description: 'Work item type returned by Azure DevOps (e.g. Issue, Task, Epic)', + }, + assignedTo: { + type: 'string', + description: 'Display name of assigned user, or null if unassigned', + }, + areaPath: { type: 'string', description: 'Area path of the work item' }, + url: { type: 'string', description: 'API URL for the created work item' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/get_build_log.ts b/apps/sim/tools/azure_devops/get_build_log.ts new file mode 100644 index 00000000000..96bea77e67d --- /dev/null +++ b/apps/sim/tools/azure_devops/get_build_log.ts @@ -0,0 +1,101 @@ +import type { GetBuildLogParams, GetBuildLogResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const getBuildLogTool: ToolConfig = { + id: 'azure_devops_get_build_log', + name: 'Azure DevOps Get Build Log', + description: + 'Fetch the text content of a specific build log in Azure DevOps. Use List Build Logs first to get the log ID. Optionally retrieve only a line range with startLine/endLine.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + buildId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The build ID containing the log', + }, + logId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The log entry ID to fetch (from List Build Logs)', + }, + startLine: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'First line to return (1-based, inclusive)', + }, + endLine: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Last line to return (1-based, inclusive)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${params.buildId}/logs/${params.logId}` + ) + url.searchParams.set('api-version', '7.2-preview.2') + if (params.startLine !== undefined) + url.searchParams.set('startLine', Number(params.startLine).toString()) + if (params.endLine !== undefined) + url.searchParams.set('endLine', Number(params.endLine).toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'text/plain', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const text = await response.text() + const trimmed = text.trim() + const lineCount = trimmed.length === 0 ? 0 : trimmed.split('\n').length + + return { + success: true, + output: { + content: trimmed.length === 0 ? 'Log is empty.' : text, + metadata: { + lineCount, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Raw log text' }, + metadata: { + type: 'object', + description: 'Log metadata', + properties: { + lineCount: { type: 'number', description: 'Number of lines in the returned log text' }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/get_build_timeline.ts b/apps/sim/tools/azure_devops/get_build_timeline.ts new file mode 100644 index 00000000000..f5f3844180f --- /dev/null +++ b/apps/sim/tools/azure_devops/get_build_timeline.ts @@ -0,0 +1,158 @@ +import type { + AzureDevOpsBuildTimelineRecord, + GetBuildTimelineParams, + GetBuildTimelineResponse, +} from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const getBuildTimelineTool: ToolConfig = { + id: 'azure_devops_get_build_timeline', + name: 'Azure DevOps Get Build Timeline', + description: + 'Get the execution timeline for an Azure DevOps build — every stage, job, and task with its result and log ID. Use this to identify which steps failed before fetching their logs with Get Build Log.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + buildId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the build whose timeline to retrieve', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read)', + }, + }, + + request: { + url: (params) => + `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${Number(params.buildId)}/timeline?api-version=7.2-preview.3`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const records: AzureDevOpsBuildTimelineRecord[] = (data.records ?? []).map( + (r: { + id: string + name: string + type: string + result: string | null + log?: { id?: number } | null + errorCount?: number + warningCount?: number + startTime?: string + finishTime?: string + }) => ({ + id: r.id, + name: r.name, + type: r.type, + result: r.result ?? null, + logId: r.log?.id ?? null, + errorCount: r.errorCount ?? 0, + warningCount: r.warningCount ?? 0, + startTime: r.startTime ?? '', + finishTime: r.finishTime ?? '', + }) + ) + + const failedRecords = records.filter((r) => r.result === 'failed') + + const content = + failedRecords.length === 0 + ? `Build timeline: ${records.length} record(s), no failures detected.` + : `Build timeline: ${records.length} record(s), ${failedRecords.length} failed:\n\n` + + failedRecords + .map( + (r) => + `[${r.type}] ${r.name} — result: ${r.result}, logId: ${r.logId ?? 'none'}, errors: ${r.errorCount}` + ) + .join('\n') + + return { + success: true, + output: { + content, + metadata: { + totalCount: records.length, + failedCount: failedRecords.length, + records, + failedRecords, + }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Summary of the build timeline, highlighting failed steps', + }, + metadata: { + type: 'object', + description: 'Build timeline metadata', + properties: { + totalCount: { type: 'number', description: 'Total number of timeline records' }, + failedCount: { type: 'number', description: 'Number of failed records' }, + records: { + type: 'array', + description: 'All timeline records (stages, jobs, tasks)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Record GUID' }, + name: { type: 'string', description: 'Step name (e.g. "Run tests")' }, + type: { type: 'string', description: 'Stage | Phase | Job | Task' }, + result: { + type: 'string', + description: 'succeeded | failed | skipped | canceled | null', + }, + logId: { type: 'number', description: 'Log ID to pass to Get Build Log, or null' }, + errorCount: { type: 'number', description: 'Number of errors' }, + warningCount: { type: 'number', description: 'Number of warnings' }, + startTime: { type: 'string', description: 'ISO 8601 start timestamp' }, + finishTime: { type: 'string', description: 'ISO 8601 finish timestamp' }, + }, + }, + }, + failedRecords: { + type: 'array', + description: 'Subset of records where result === "failed" — use logId to fetch logs', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Record GUID' }, + name: { type: 'string', description: 'Step name' }, + type: { type: 'string', description: 'Stage | Phase | Job | Task' }, + result: { type: 'string', description: 'failed' }, + logId: { type: 'number', description: 'Log ID to pass to Get Build Log' }, + errorCount: { type: 'number', description: 'Number of errors' }, + warningCount: { type: 'number', description: 'Number of warnings' }, + startTime: { type: 'string', description: 'ISO 8601 start timestamp' }, + finishTime: { type: 'string', description: 'ISO 8601 finish timestamp' }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/get_comments.ts b/apps/sim/tools/azure_devops/get_comments.ts new file mode 100644 index 00000000000..0c5328e59e0 --- /dev/null +++ b/apps/sim/tools/azure_devops/get_comments.ts @@ -0,0 +1,186 @@ +import type { + AzureDevOpsComment, + GetCommentsParams, + GetCommentsResponse, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsRawComment } from '@/tools/azure_devops/utils' +import { formatComment, mapComment } from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const getCommentsTool: ToolConfig = { + id: 'azure_devops_get_comments', + name: 'Azure DevOps Get Comments', + description: 'List comments for an Azure DevOps work item.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + workItemId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the work item whose comments should be listed', + }, + top: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of comments to return', + }, + continuationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Continuation token for paginating comments', + }, + includeDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether deleted comments should be returned', + }, + expand: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Additional comment data to include: none, reactions, renderedText, renderedTextOnly, all', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order for comments: asc or desc', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workItems/${Number(params.workItemId)}/comments` + ) + url.searchParams.set('api-version', '7.2-preview.4') + if (params.top) url.searchParams.set('$top', Number(params.top).toString()) + if (params.continuationToken) + url.searchParams.set('continuationToken', params.continuationToken) + if (params.includeDeleted !== undefined) + url.searchParams.set('includeDeleted', String(params.includeDeleted)) + if (params.expand) url.searchParams.set('$expand', params.expand) + if (params.order) url.searchParams.set('order', params.order) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const comments: AzureDevOpsComment[] = (data.comments ?? []).map((raw: AzureDevOpsRawComment) => + mapComment(raw) + ) + + const content = + comments.length === 0 + ? 'No comments found for this work item.' + : `Found ${data.count ?? comments.length} comment(s):\n\n${comments + .map(formatComment) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? comments.length, + totalCount: data.totalCount ?? comments.length, + comments, + continuationToken: data.continuationToken, + nextPage: data.nextPage, + url: data.url, + }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of work item comments', + }, + metadata: { + type: 'object', + description: 'Comments metadata', + properties: { + count: { type: 'number', description: 'Number of comments returned in this page' }, + totalCount: { type: 'number', description: 'Total number of comments on the work item' }, + continuationToken: { + type: 'string', + description: 'Continuation token for the next page', + optional: true, + }, + nextPage: { + type: 'string', + description: 'API URL for the next page', + optional: true, + }, + url: { + type: 'string', + description: 'API URL for this comments list', + optional: true, + }, + comments: { + type: 'array', + description: 'Array of work item comments', + items: { + type: 'object', + properties: { + workItemId: { type: 'number', description: 'Work item ID' }, + commentId: { type: 'number', description: 'Comment ID' }, + version: { type: 'number', description: 'Comment version' }, + text: { type: 'string', description: 'Comment text' }, + renderedText: { + type: 'string', + description: 'Rendered HTML comment text when available', + optional: true, + }, + createdBy: { + type: 'string', + description: 'Display name of the comment author', + nullable: true, + }, + createdDate: { type: 'string', description: 'ISO 8601 creation timestamp' }, + modifiedBy: { + type: 'string', + description: 'Display name of the last modifier', + nullable: true, + }, + modifiedDate: { type: 'string', description: 'ISO 8601 modified timestamp' }, + isDeleted: { type: 'boolean', description: 'Whether the comment is deleted' }, + url: { type: 'string', description: 'API URL for the comment' }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/get_pipeline.ts b/apps/sim/tools/azure_devops/get_pipeline.ts new file mode 100644 index 00000000000..4bad50ea5e1 --- /dev/null +++ b/apps/sim/tools/azure_devops/get_pipeline.ts @@ -0,0 +1,167 @@ +import type { GetPipelineParams, GetPipelineResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const getPipelineTool: ToolConfig = { + id: 'azure_devops_get_pipeline', + name: 'Azure DevOps Get Pipeline', + description: + 'Get details for a specific pipeline in an Azure DevOps project, including configuration and repository info.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + pipelineId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the pipeline to retrieve', + }, + pipelineVersion: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Specific revision of the pipeline to retrieve (defaults to latest)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read, Pipeline: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}` + ) + url.searchParams.set('api-version', '7.2-preview.1') + if (params.pipelineVersion) + url.searchParams.set('pipelineVersion', Number(params.pipelineVersion).toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const pipeline: AzureDevOpsPipelineDetailItem = { + id: data.id, + name: data.name, + folder: data.folder ?? '\\', + revision: data.revision, + url: data.url, + configuration: { + type: data.configuration?.type ?? 'unknown', + path: data.configuration?.path, + repository: data.configuration?.repository + ? { id: data.configuration.repository.id, type: data.configuration.repository.type } + : undefined, + }, + links: { + self: data._links?.self?.href ?? '', + web: data._links?.web?.href ?? '', + }, + } + + const pathLine = pipeline.configuration.path ? `\n Path: ${pipeline.configuration.path}` : '' + const repoLine = pipeline.configuration.repository + ? `\n Repository: ${pipeline.configuration.repository.id} (${pipeline.configuration.repository.type})` + : '' + + const content = + `Pipeline: ${pipeline.name} (ID: ${pipeline.id})\n` + + `Folder: ${pipeline.folder}\n` + + `Revision: ${pipeline.revision}\n` + + `Config type: ${pipeline.configuration.type}` + + pathLine + + repoLine + + `\nWeb URL: ${pipeline.links.web}` + + return { + success: true, + output: { + content, + metadata: { pipeline }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of the pipeline' }, + metadata: { + type: 'object', + description: 'Pipeline detail metadata', + properties: { + pipeline: { + type: 'object', + description: 'Full pipeline detail object', + properties: { + id: { type: 'number', description: 'Pipeline ID' }, + name: { type: 'string', description: 'Pipeline name' }, + folder: { type: 'string', description: 'Folder path' }, + revision: { type: 'number', description: 'Pipeline revision number' }, + url: { type: 'string', description: 'Pipeline API URL' }, + configuration: { + type: 'object', + description: 'Pipeline configuration', + properties: { + type: { type: 'string', description: 'Configuration type (e.g. "yaml")' }, + path: { type: 'string', description: 'YAML file path in the repository' }, + repository: { + type: 'object', + description: 'Source repository info', + properties: { + id: { type: 'string', description: 'Repository ID' }, + type: { + type: 'string', + description: 'Repository type (e.g. "azureReposGit")', + }, + }, + }, + }, + }, + links: { + type: 'object', + description: 'Hypermedia links', + properties: { + self: { type: 'string', description: 'API self-link' }, + web: { type: 'string', description: 'Browser URL for the pipeline' }, + }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsPipelineDetailItem { + id: number + name: string + folder: string + revision: number + url: string + configuration: { + type: string + path?: string + repository?: { id: string; type: string } + } + links: { self: string; web: string } +} diff --git a/apps/sim/tools/azure_devops/get_pipeline_run.ts b/apps/sim/tools/azure_devops/get_pipeline_run.ts new file mode 100644 index 00000000000..89db7f7804e --- /dev/null +++ b/apps/sim/tools/azure_devops/get_pipeline_run.ts @@ -0,0 +1,157 @@ +import type { GetPipelineRunParams, GetPipelineRunResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const getPipelineRunTool: ToolConfig = { + id: 'azure_devops_get_pipeline_run', + name: 'Azure DevOps Get Pipeline Run', + description: + 'Get details for a specific pipeline run in an Azure DevOps project. Returns run state, result, timestamps, and the pipeline reference.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + pipelineId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the pipeline', + }, + runId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the run to retrieve', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read, Pipeline: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}/runs/${params.runId}` + ) + url.searchParams.set('api-version', '7.2-preview.1') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const run: AzureDevOpsPipelineRunDetailItem = { + id: data.id, + name: data.name, + state: data.state, + result: data.result, + createdDate: data.createdDate, + finishedDate: data.finishedDate, + url: data.url, + webUrl: data._links?.web?.href ?? '', + pipeline: { + id: data.pipeline?.id, + name: data.pipeline?.name, + folder: data.pipeline?.folder ?? '\\', + revision: data.pipeline?.revision, + url: data.pipeline?.url ?? '', + }, + } + + const resultLine = run.result ? ` | Result: ${run.result}` : '' + const finishedLine = run.finishedDate ? ` | Finished: ${run.finishedDate}` : '' + + const content = + `Run: ${run.name} (ID: ${run.id})\n` + + `Pipeline: ${run.pipeline.name} (ID: ${run.pipeline.id})\n` + + `State: ${run.state}${resultLine}\n` + + `Created: ${run.createdDate}${finishedLine}\n` + + `Web URL: ${run.webUrl}` + + return { + success: true, + output: { + content, + metadata: { run }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of the pipeline run' }, + metadata: { + type: 'object', + description: 'Pipeline run metadata', + properties: { + run: { + type: 'object', + description: 'Full pipeline run detail object', + properties: { + id: { type: 'number', description: 'Run ID' }, + name: { type: 'string', description: 'Run name (e.g. "20210601.1")' }, + state: { type: 'string', description: 'Run state (e.g. "completed", "inProgress")' }, + result: { + type: 'string', + description: 'Run result (e.g. "succeeded", "failed") — absent if still running', + }, + createdDate: { type: 'string', description: 'ISO 8601 creation timestamp' }, + finishedDate: { + type: 'string', + description: 'ISO 8601 finish timestamp — absent if still running', + }, + url: { type: 'string', description: 'Run API URL' }, + webUrl: { type: 'string', description: 'Browser URL for the run' }, + pipeline: { + type: 'object', + description: 'Pipeline reference', + properties: { + id: { type: 'number', description: 'Pipeline ID' }, + name: { type: 'string', description: 'Pipeline name' }, + folder: { type: 'string', description: 'Pipeline folder' }, + revision: { type: 'number', description: 'Pipeline revision number' }, + url: { type: 'string', description: 'Pipeline API URL' }, + }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsPipelineRunDetailItem { + id: number + name: string + state: string + result?: string + createdDate: string + finishedDate?: string + url: string + webUrl: string + pipeline: { + id: number + name: string + folder: string + revision: number + url: string + } +} diff --git a/apps/sim/tools/azure_devops/get_work_item.ts b/apps/sim/tools/azure_devops/get_work_item.ts new file mode 100644 index 00000000000..667266474df --- /dev/null +++ b/apps/sim/tools/azure_devops/get_work_item.ts @@ -0,0 +1,103 @@ +import type { GetWorkItemParams, GetWorkItemResponse } from '@/tools/azure_devops/types' +import type { AzureDevOpsRawWorkItem } from '@/tools/azure_devops/utils' +import { formatWorkItem, mapWorkItem } from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const getWorkItemTool: ToolConfig = { + id: 'azure_devops_get_work_item', + name: 'Azure DevOps Get Work Item', + description: + 'Fetch full details of a single work item by ID from Azure DevOps, including title, state, type, assignee, and area path.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + workItemId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The work item ID to fetch', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}` + ) + url.searchParams.set('$expand', 'all') + url.searchParams.set('api-version', '7.2-preview.3') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const raw: AzureDevOpsRawWorkItem = await response.json() + const workItem = mapWorkItem(raw) + + return { + success: true, + output: { + content: formatWorkItem(workItem), + metadata: { workItem }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of the work item', + }, + metadata: { + type: 'object', + description: 'Work item metadata', + properties: { + workItem: { + type: 'object', + description: 'Full work item details', + properties: { + id: { type: 'number', description: 'Work item ID' }, + title: { type: 'string', description: 'Work item title' }, + state: { + type: 'string', + description: 'Current state for Basic process (e.g. To Do, Doing, Done)', + }, + workItemType: { + type: 'string', + description: 'Work item type returned by Azure DevOps (e.g. Issue, Task, Epic)', + }, + assignedTo: { + type: 'string', + description: 'Display name of assigned user, or null if unassigned', + }, + areaPath: { type: 'string', description: 'Area path of the work item' }, + url: { type: 'string', description: 'API URL for the work item' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/get_work_items_batch.ts b/apps/sim/tools/azure_devops/get_work_items_batch.ts new file mode 100644 index 00000000000..0ef221b7308 --- /dev/null +++ b/apps/sim/tools/azure_devops/get_work_items_batch.ts @@ -0,0 +1,121 @@ +import type { + AzureDevOpsWorkItem, + GetWorkItemsBatchParams, + GetWorkItemsBatchResponse, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsRawWorkItem } from '@/tools/azure_devops/utils' +import { formatWorkItem, mapWorkItem } from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const getWorkItemsBatchTool: ToolConfig = + { + id: 'azure_devops_get_work_items_batch', + name: 'Azure DevOps Get Work Items Batch', + description: + 'Fetch full details for multiple work items by ID from Azure DevOps in a single call. Pass comma-separated IDs (e.g. "123,456,789"). Maximum 200 IDs per request.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + ids: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated work item IDs to fetch (e.g. "123,456,789"). Maximum 200 IDs.', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems` + ) + url.searchParams.set('ids', params.ids) + url.searchParams.set('$expand', 'all') + url.searchParams.set('api-version', '7.2-preview.3') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const workItems: AzureDevOpsWorkItem[] = (data.value ?? []).map( + (raw: AzureDevOpsRawWorkItem) => mapWorkItem(raw) + ) + + const content = + workItems.length === 0 + ? 'No work items found for the provided IDs.' + : `Found ${workItems.length} work item(s):\n\n${workItems.map(formatWorkItem).join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { count: workItems.length, workItems }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of the fetched work items', + }, + metadata: { + type: 'object', + description: 'Work items metadata', + properties: { + count: { type: 'number', description: 'Number of work items returned' }, + workItems: { + type: 'array', + description: 'Array of work item details', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Work item ID' }, + title: { type: 'string', description: 'Work item title' }, + state: { + type: 'string', + description: 'Current state for Basic process (e.g. To Do, Doing, Done)', + }, + workItemType: { + type: 'string', + description: 'Work item type returned by Azure DevOps (e.g. Issue, Task, Epic)', + }, + assignedTo: { + type: 'string', + description: 'Display name of assigned user, or null if unassigned', + }, + areaPath: { type: 'string', description: 'Area path of the work item' }, + url: { type: 'string', description: 'API URL for the work item' }, + }, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/azure_devops/get_work_items_between_builds.ts b/apps/sim/tools/azure_devops/get_work_items_between_builds.ts new file mode 100644 index 00000000000..09400c9abb4 --- /dev/null +++ b/apps/sim/tools/azure_devops/get_work_items_between_builds.ts @@ -0,0 +1,126 @@ +import type { + AzureDevOpsWorkItemRef, + GetWorkItemsBetweenBuildsParams, + GetWorkItemsBetweenBuildsResponse, +} from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const getWorkItemsBetweenBuildsTool: ToolConfig< + GetWorkItemsBetweenBuildsParams, + GetWorkItemsBetweenBuildsResponse +> = { + id: 'azure_devops_get_work_items_between_builds', + name: 'Azure DevOps Get Work Items Between Builds', + description: + 'Get work item references associated with commits between two builds in Azure DevOps. Returns work item IDs and URLs — use Get Work Items Batch for full field details.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + fromBuildId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The older build ID (start of range)', + }, + toBuildId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The newer build ID (end of range)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/workitems` + ) + url.searchParams.set('fromBuildId', Number(params.fromBuildId).toString()) + url.searchParams.set('toBuildId', Number(params.toBuildId).toString()) + url.searchParams.set('api-version', '7.2-preview.2') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const workItems: AzureDevOpsWorkItemRef[] = (data.value ?? []).map( + (w: AzureDevOpsRawWorkItemRef) => ({ + id: String(w.id), + url: w.url, + }) + ) + + const content = + workItems.length === 0 + ? 'No work items found between these builds.' + : `Found ${data.count ?? workItems.length} work item(s) between builds:\n\n${workItems + .map((w) => `- Work Item ID: ${w.id}\n URL: ${w.url}`) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? workItems.length, + workItems, + }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of work items between builds', + }, + metadata: { + type: 'object', + description: 'Work items metadata', + properties: { + count: { type: 'number', description: 'Total number of work item references returned' }, + workItems: { + type: 'array', + description: 'Array of work item references', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Work item ID' }, + url: { type: 'string', description: 'API URL for the work item' }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsRawWorkItemRef { + id: string | number + url: string +} diff --git a/apps/sim/tools/azure_devops/index.ts b/apps/sim/tools/azure_devops/index.ts new file mode 100644 index 00000000000..0d381d9c47f --- /dev/null +++ b/apps/sim/tools/azure_devops/index.ts @@ -0,0 +1,35 @@ +import { addCommentTool } from '@/tools/azure_devops/add_comment' +import { createWorkItemTool } from '@/tools/azure_devops/create_work_item' +import { getBuildLogTool } from '@/tools/azure_devops/get_build_log' +import { getBuildTimelineTool } from '@/tools/azure_devops/get_build_timeline' +import { getCommentsTool } from '@/tools/azure_devops/get_comments' +import { getPipelineTool } from '@/tools/azure_devops/get_pipeline' +import { getPipelineRunTool } from '@/tools/azure_devops/get_pipeline_run' +import { getWorkItemTool } from '@/tools/azure_devops/get_work_item' +import { getWorkItemsBatchTool } from '@/tools/azure_devops/get_work_items_batch' +import { getWorkItemsBetweenBuildsTool } from '@/tools/azure_devops/get_work_items_between_builds' +import { listBuildLogsTool } from '@/tools/azure_devops/list_build_logs' +import { listBuildsTool } from '@/tools/azure_devops/list_builds' +import { listPipelineRunsTool } from '@/tools/azure_devops/list_pipeline_runs' +import { listPipelinesTool } from '@/tools/azure_devops/list_pipelines' +import { queryWorkItemsTool } from '@/tools/azure_devops/query_work_items' +import { updateWorkItemTool } from '@/tools/azure_devops/update_work_item' + +export { + listPipelinesTool, + getPipelineTool, + listPipelineRunsTool, + getPipelineRunTool, + listBuildsTool, + listBuildLogsTool, + getBuildLogTool, + getBuildTimelineTool, + getWorkItemsBetweenBuildsTool, + queryWorkItemsTool, + getWorkItemTool, + getWorkItemsBatchTool, + createWorkItemTool, + updateWorkItemTool, + addCommentTool, + getCommentsTool, +} diff --git a/apps/sim/tools/azure_devops/list_build_logs.ts b/apps/sim/tools/azure_devops/list_build_logs.ts new file mode 100644 index 00000000000..d57a24eec59 --- /dev/null +++ b/apps/sim/tools/azure_devops/list_build_logs.ts @@ -0,0 +1,134 @@ +import type { ListBuildLogsParams, ListBuildLogsResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const listBuildLogsTool: ToolConfig = { + id: 'azure_devops_list_build_logs', + name: 'Azure DevOps List Build Logs', + description: + 'List all log entries for a specific build in Azure DevOps. Returns log IDs, types, and line counts — use the log ID with the Get Build Log tool to fetch actual log text.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + buildId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The build ID whose logs to list', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read)', + }, + }, + + request: { + url: (params) => + `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${params.buildId}/logs?api-version=7.2-preview.2`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const logs: AzureDevOpsBuildLogItem[] = (data.value ?? []).map((l: AzureDevOpsRawBuildLog) => ({ + id: l.id, + type: l.type, + url: l.url, + lineCount: l.lineCount, + createdOn: l.createdOn, + lastChangedOn: l.lastChangedOn, + })) + + const content = + logs.length === 0 + ? 'No logs found.' + : `Found ${data.count ?? logs.length} log(s):\n\n${logs + .map( + (l) => + `- Log ID: ${l.id}\n` + + ` Type: ${l.type}\n` + + ` Lines: ${l.lineCount}` + + (l.lastChangedOn ? `\n Last changed: ${l.lastChangedOn}` : '') + ) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? logs.length, + logs, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of build logs' }, + metadata: { + type: 'object', + description: 'Build logs metadata', + properties: { + count: { type: 'number', description: 'Total number of log entries returned' }, + logs: { + type: 'array', + description: 'Array of log entry objects', + items: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'Log entry ID — use with Get Build Log to fetch content', + }, + type: { + type: 'string', + description: 'Log type (e.g. "Container", "Task", "Section")', + }, + url: { type: 'string', description: 'API URL for the log entry' }, + lineCount: { type: 'number', description: 'Number of lines in the log' }, + createdOn: { type: 'string', description: 'ISO 8601 creation timestamp' }, + lastChangedOn: { type: 'string', description: 'ISO 8601 last-changed timestamp' }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsBuildLogItem { + id: number + type: string + url: string + lineCount: number + createdOn?: string + lastChangedOn?: string +} + +interface AzureDevOpsRawBuildLog { + id: number + type: string + url: string + lineCount: number + createdOn?: string + lastChangedOn?: string +} diff --git a/apps/sim/tools/azure_devops/list_builds.ts b/apps/sim/tools/azure_devops/list_builds.ts new file mode 100644 index 00000000000..34fe1b5ef73 --- /dev/null +++ b/apps/sim/tools/azure_devops/list_builds.ts @@ -0,0 +1,203 @@ +import type { ListBuildsParams, ListBuildsResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const listBuildsTool: ToolConfig = { + id: 'azure_devops_list_builds', + name: 'Azure DevOps List Builds', + description: + 'List builds in an Azure DevOps project. Optionally filter by pipeline definition, status, result, or branch.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + definitionIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated pipeline definition IDs to filter by (e.g. "1,2,3")', + }, + top: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of builds to return', + }, + statusFilter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by build status: inProgress, completed, cancelling, postponed, notStarted, none', + }, + resultFilter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by build result: succeeded, partiallySucceeded, failed, canceled', + }, + branchName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by source branch name (e.g. "refs/heads/main")', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds` + ) + url.searchParams.set('api-version', '7.2-preview.8') + if (params.definitionIds) url.searchParams.set('definitions', params.definitionIds) + if (params.top) url.searchParams.set('$top', Number(params.top).toString()) + if (params.statusFilter) url.searchParams.set('statusFilter', params.statusFilter) + if (params.resultFilter) url.searchParams.set('resultFilter', params.resultFilter) + if (params.branchName) url.searchParams.set('branchName', params.branchName) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const builds: AzureDevOpsBuildItem[] = (data.value ?? []).map((b: AzureDevOpsRawBuild) => ({ + id: b.id, + buildNumber: b.buildNumber, + status: b.status, + result: b.result, + queueTime: b.queueTime, + startTime: b.startTime, + finishTime: b.finishTime, + sourceBranch: b.sourceBranch, + sourceVersion: b.sourceVersion, + definition: { id: b.definition?.id ?? 0, name: b.definition?.name ?? '' }, + webUrl: b._links?.web?.href ?? '', + })) + + const content = + builds.length === 0 + ? 'No builds found.' + : `Found ${data.count} build(s):\n\n${builds + .map( + (b) => + `- Build ${b.buildNumber} (ID: ${b.id})\n` + + ` Pipeline: ${b.definition.name}\n` + + ` Status: ${b.status}${b.result ? ` | Result: ${b.result}` : ''}\n` + + ` Branch: ${b.sourceBranch}\n` + + ` Queued: ${b.queueTime}${b.finishTime ? ` | Finished: ${b.finishTime}` : ''}` + ) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? builds.length, + builds, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of builds' }, + metadata: { + type: 'object', + description: 'Builds metadata', + properties: { + count: { type: 'number', description: 'Total number of builds returned' }, + builds: { + type: 'array', + description: 'Array of build objects', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Build ID' }, + buildNumber: { type: 'string', description: 'Build number (e.g. "20210601.1")' }, + status: { + type: 'string', + description: 'Build status (e.g. "completed", "inProgress")', + }, + result: { + type: 'string', + description: 'Build result (e.g. "succeeded", "failed") — absent if still running', + }, + queueTime: { type: 'string', description: 'ISO 8601 queue timestamp' }, + startTime: { type: 'string', description: 'ISO 8601 start timestamp' }, + finishTime: { + type: 'string', + description: 'ISO 8601 finish timestamp — absent if still running', + }, + sourceBranch: { + type: 'string', + description: 'Source branch (e.g. "refs/heads/main")', + }, + sourceVersion: { type: 'string', description: 'Source commit SHA' }, + definition: { + type: 'object', + description: 'Pipeline definition reference', + properties: { + id: { type: 'number', description: 'Definition ID' }, + name: { type: 'string', description: 'Definition name' }, + }, + }, + webUrl: { type: 'string', description: 'Browser URL for the build' }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsBuildItem { + id: number + buildNumber: string + status: string + result?: string + queueTime: string + startTime?: string + finishTime?: string + sourceBranch: string + sourceVersion: string + definition: { id: number; name: string } + webUrl: string +} + +interface AzureDevOpsRawBuild { + id: number + buildNumber: string + status: string + result?: string + queueTime: string + startTime?: string + finishTime?: string + sourceBranch: string + sourceVersion: string + definition?: { id: number; name?: string } + _links?: { web?: { href: string } } +} diff --git a/apps/sim/tools/azure_devops/list_pipeline_runs.ts b/apps/sim/tools/azure_devops/list_pipeline_runs.ts new file mode 100644 index 00000000000..c4dbbc54db3 --- /dev/null +++ b/apps/sim/tools/azure_devops/list_pipeline_runs.ts @@ -0,0 +1,149 @@ +import type { ListPipelineRunsParams, ListPipelineRunsResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const listPipelineRunsTool: ToolConfig = { + id: 'azure_devops_list_pipeline_runs', + name: 'Azure DevOps List Pipeline Runs', + description: + 'List runs for a specific pipeline in an Azure DevOps project. Returns run ID, name, state, result, and timestamps.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + pipelineId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the pipeline whose runs to list', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read, Pipeline: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}/runs` + ) + url.searchParams.set('api-version', '7.2-preview.1') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const runs: AzureDevOpsPipelineRunItem[] = (data.value ?? []).map((r: AzureDevOpsRawRun) => ({ + id: r.id, + name: r.name, + state: r.state, + result: r.result, + createdDate: r.createdDate, + finishedDate: r.finishedDate, + url: r.url, + webUrl: r._links?.web?.href ?? '', + })) + + const content = + runs.length === 0 + ? 'No pipeline runs found.' + : `Found ${data.count} run(s):\n\n${runs + .map( + (r) => + `- Run ${r.name} (ID: ${r.id})\n` + + ` State: ${r.state}${r.result ? ` | Result: ${r.result}` : ''}\n` + + ` Created: ${r.createdDate}${r.finishedDate ? ` | Finished: ${r.finishedDate}` : ''}` + ) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? runs.length, + runs, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of pipeline runs' }, + metadata: { + type: 'object', + description: 'Pipeline runs metadata', + properties: { + count: { type: 'number', description: 'Total number of runs returned' }, + runs: { + type: 'array', + description: 'Array of pipeline run objects', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Run ID' }, + name: { type: 'string', description: 'Run name (e.g. "20210601.1")' }, + state: { + type: 'string', + description: 'Run state (e.g. "completed", "inProgress")', + }, + result: { + type: 'string', + description: 'Run result (e.g. "succeeded", "failed") — absent if still running', + }, + createdDate: { type: 'string', description: 'ISO 8601 creation timestamp' }, + finishedDate: { + type: 'string', + description: 'ISO 8601 finish timestamp — absent if still running', + }, + url: { type: 'string', description: 'Run API URL' }, + webUrl: { type: 'string', description: 'Browser URL for the run' }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsPipelineRunItem { + id: number + name: string + state: string + result?: string + createdDate: string + finishedDate?: string + url: string + webUrl: string +} + +interface AzureDevOpsRawRun { + id: number + name: string + state: string + result?: string + createdDate: string + finishedDate?: string + url: string + _links?: { web?: { href: string } } +} diff --git a/apps/sim/tools/azure_devops/list_pipelines.ts b/apps/sim/tools/azure_devops/list_pipelines.ts new file mode 100644 index 00000000000..11de874545d --- /dev/null +++ b/apps/sim/tools/azure_devops/list_pipelines.ts @@ -0,0 +1,133 @@ +import type { ListPipelinesParams, ListPipelinesResponse } from '@/tools/azure_devops/types' +import type { ToolConfig } from '@/tools/types' + +export const listPipelinesTool: ToolConfig = { + id: 'azure_devops_list_pipelines', + name: 'Azure DevOps List Pipelines', + description: + 'List all pipelines in an Azure DevOps project. Returns pipeline ID, name, folder, revision, and URL.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Field to sort results by (e.g. "name")', + }, + top: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of pipelines to return', + }, + continuationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Continuation token for paginating results', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Build: Read, Pipeline: Read)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines` + ) + url.searchParams.set('api-version', '7.2-preview.1') + if (params.orderBy) url.searchParams.set('orderBy', params.orderBy) + if (params.top) url.searchParams.set('$top', Number(params.top).toString()) + if (params.continuationToken) + url.searchParams.set('continuationToken', params.continuationToken) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const pipelines: AzureDevOpsPipelineItem[] = (data.value ?? []).map( + (p: AzureDevOpsPipelineItem) => ({ + id: p.id, + name: p.name, + folder: p.folder ?? '\\', + revision: p.revision, + url: p.url, + }) + ) + + const content = + pipelines.length === 0 + ? 'No pipelines found.' + : `Found ${data.count} pipeline(s):\n\n${pipelines + .map((p) => `- ${p.name} (ID: ${p.id})\n Folder: ${p.folder}\n URL: ${p.url}`) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + count: data.count ?? pipelines.length, + pipelines, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable summary of pipelines' }, + metadata: { + type: 'object', + description: 'Pipelines metadata', + properties: { + count: { type: 'number', description: 'Total number of pipelines returned' }, + pipelines: { + type: 'array', + description: 'Array of pipeline objects', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Pipeline ID' }, + name: { type: 'string', description: 'Pipeline name' }, + folder: { type: 'string', description: 'Folder path (e.g. "\\\\")' }, + revision: { type: 'number', description: 'Pipeline revision number' }, + url: { type: 'string', description: 'Pipeline API URL' }, + }, + }, + }, + }, + }, + }, +} + +interface AzureDevOpsPipelineItem { + id: number + name: string + folder: string + revision: number + url: string +} diff --git a/apps/sim/tools/azure_devops/query_work_items.ts b/apps/sim/tools/azure_devops/query_work_items.ts new file mode 100644 index 00000000000..de6aef1147b --- /dev/null +++ b/apps/sim/tools/azure_devops/query_work_items.ts @@ -0,0 +1,147 @@ +import type { + AzureDevOpsWorkItem, + QueryWorkItemsParams, + QueryWorkItemsResponse, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsRawWorkItem } from '@/tools/azure_devops/utils' +import { formatWorkItem, mapWorkItem } from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const queryWorkItemsTool: ToolConfig = { + id: 'azure_devops_query_work_items', + name: 'Azure DevOps Query Work Items', + description: + 'Execute a WIQL query to search for work items in Azure DevOps and return full field details. Use TOP N in your query to limit results (Azure enforces a 200-item maximum per batch fetch).', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + wiqlQuery: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'WIQL query string (e.g. "SELECT [System.Id] FROM workitems WHERE [System.State] = \'Doing\' ORDER BY [System.Id] ASC"). Use TOP N to limit results.', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read)', + }, + }, + + request: { + url: (params) => + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/wiql?api-version=7.2-preview.2`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + body: (params) => ({ query: params.wiqlQuery }), + }, + + transformResponse: async (response, params) => { + const wiqlData = await response.json() + const workItemRefs: Array<{ id: number; url: string }> = wiqlData.workItems ?? [] + + if (workItemRefs.length === 0) { + return { + success: true, + output: { + content: 'No work items matched the query.', + metadata: { count: 0, workItems: [] }, + }, + } + } + + const ids = workItemRefs + .slice(0, 200) + .map((wi) => wi.id) + .join(',') + + const detailsUrl = new URL( + `https://dev.azure.com/${params!.organization}/${params!.project}/_apis/wit/workitems` + ) + detailsUrl.searchParams.set('ids', ids) + detailsUrl.searchParams.set('$expand', 'all') + detailsUrl.searchParams.set('api-version', '7.2-preview.3') + + const detailsResponse = await fetch(detailsUrl.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`:${params!.accessToken}`)}`, + }, + }) + + const detailsData = await detailsResponse.json() + const workItems: AzureDevOpsWorkItem[] = (detailsData.value ?? []).map( + (raw: AzureDevOpsRawWorkItem) => mapWorkItem(raw) + ) + + const content = + workItems.length === 0 + ? 'No work item details found.' + : `Found ${workItems.length} work item(s):\n\n${workItems.map(formatWorkItem).join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { count: workItems.length, workItems }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of matching work items', + }, + metadata: { + type: 'object', + description: 'Work items metadata', + properties: { + count: { type: 'number', description: 'Number of work items returned' }, + workItems: { + type: 'array', + description: 'Array of work item details', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Work item ID' }, + title: { type: 'string', description: 'Work item title' }, + state: { + type: 'string', + description: 'Current state for Basic process (e.g. To Do, Doing, Done)', + }, + workItemType: { + type: 'string', + description: 'Work item type returned by Azure DevOps (e.g. Issue, Task, Epic)', + }, + assignedTo: { + type: 'string', + description: 'Display name of assigned user, or null if unassigned', + }, + areaPath: { type: 'string', description: 'Area path of the work item' }, + url: { type: 'string', description: 'API URL for the work item' }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/types.ts b/apps/sim/tools/azure_devops/types.ts new file mode 100644 index 00000000000..17f2bee998c --- /dev/null +++ b/apps/sim/tools/azure_devops/types.ts @@ -0,0 +1,456 @@ +import type { ToolResponse } from '@/tools/types' + +export interface AzureDevOpsBaseParams { + /** Azure DevOps organization name */ + organization: string + /** Azure DevOps project name */ + project: string + /** Personal Access Token */ + accessToken: string +} + +// ── List Pipelines ────────────────────────────────────────────────────────── + +export interface ListPipelinesParams extends AzureDevOpsBaseParams { + orderBy?: string + top?: number + continuationToken?: string +} + +export interface AzureDevOpsPipeline { + id: number + name: string + folder: string + revision: number + url: string +} + +export interface ListPipelinesResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + pipelines: AzureDevOpsPipeline[] + } + } +} + +// ── Get Pipeline ──────────────────────────────────────────────────────────── + +export interface GetPipelineParams extends AzureDevOpsBaseParams { + pipelineId: number + pipelineVersion?: number +} + +export interface AzureDevOpsPipelineConfiguration { + type: string + path?: string + repository?: { + id: string + type: string + } +} + +export interface AzureDevOpsPipelineDetail extends AzureDevOpsPipeline { + configuration: AzureDevOpsPipelineConfiguration + links: { + self: string + web: string + } +} + +export interface GetPipelineResponse extends ToolResponse { + output: { + content: string + metadata: { + pipeline: AzureDevOpsPipelineDetail + } + } +} + +// ── List Pipeline Runs ────────────────────────────────────────────────────── + +export interface ListPipelineRunsParams extends AzureDevOpsBaseParams { + pipelineId: number +} + +export interface AzureDevOpsPipelineRun { + id: number + name: string + state: string + result?: string + createdDate: string + finishedDate?: string + url: string + webUrl: string +} + +export interface ListPipelineRunsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + runs: AzureDevOpsPipelineRun[] + } + } +} + +// ── Get Pipeline Run ──────────────────────────────────────────────────────── + +export interface GetPipelineRunParams extends AzureDevOpsBaseParams { + pipelineId: number + runId: number +} + +export interface AzureDevOpsPipelineRunDetail extends AzureDevOpsPipelineRun { + pipeline: { + id: number + name: string + folder: string + revision: number + url: string + } +} + +export interface GetPipelineRunResponse extends ToolResponse { + output: { + content: string + metadata: { + run: AzureDevOpsPipelineRunDetail + } + } +} + +// ── List Builds ───────────────────────────────────────────────────────────── + +export interface ListBuildsParams extends AzureDevOpsBaseParams { + definitionIds?: string + top?: number + statusFilter?: string + resultFilter?: string + branchName?: string +} + +export interface AzureDevOpsBuild { + id: number + buildNumber: string + status: string + result?: string + queueTime: string + startTime?: string + finishTime?: string + sourceBranch: string + sourceVersion: string + definition: { + id: number + name: string + } + webUrl: string +} + +export interface ListBuildsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + builds: AzureDevOpsBuild[] + } + } +} + +// ── List Build Logs ───────────────────────────────────────────────────────── + +export interface ListBuildLogsParams extends AzureDevOpsBaseParams { + buildId: number +} + +export interface AzureDevOpsBuildLog { + id: number + type: string + url: string + lineCount: number + createdOn?: string + lastChangedOn?: string +} + +export interface ListBuildLogsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + logs: AzureDevOpsBuildLog[] + } + } +} + +// ── Get Build Log ──────────────────────────────────────────────────────────── + +export interface GetBuildLogParams extends AzureDevOpsBaseParams { + buildId: number + logId: number + startLine?: number + endLine?: number +} + +export interface GetBuildLogResponse extends ToolResponse { + output: { + content: string + metadata: { + lineCount: number + } + } +} + +// ── Get Build Timeline ──────────────────────────────────────────────────────── + +export interface GetBuildTimelineParams extends AzureDevOpsBaseParams { + buildId: number +} + +export interface AzureDevOpsBuildTimelineRecord { + id: string + name: string + type: string + result: string | null + logId: number | null + errorCount: number + warningCount: number + startTime: string + finishTime: string +} + +export interface GetBuildTimelineResponse extends ToolResponse { + output: { + content: string + metadata: { + totalCount: number + failedCount: number + records: AzureDevOpsBuildTimelineRecord[] + failedRecords: AzureDevOpsBuildTimelineRecord[] + } + } +} + +// ── Get Work Items Between Builds ──────────────────────────────────────────── + +export interface GetWorkItemsBetweenBuildsParams extends AzureDevOpsBaseParams { + fromBuildId: number + toBuildId: number +} + +export interface AzureDevOpsWorkItemRef { + id: string + url: string +} + +export interface GetWorkItemsBetweenBuildsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + workItems: AzureDevOpsWorkItemRef[] + } + } +} + +// ── Query Work Items ───────────────────────────────────────────────────────── + +export interface QueryWorkItemsParams extends AzureDevOpsBaseParams { + wiqlQuery: string +} + +export interface AzureDevOpsWorkItem { + id: number + title: string + state: string + workItemType: string + assignedTo: string | null + areaPath: string + url: string +} + +export interface QueryWorkItemsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + workItems: AzureDevOpsWorkItem[] + } + } +} + +// ── Get Work Item ───────────────────────────────────────────────────────────── + +export interface GetWorkItemParams extends AzureDevOpsBaseParams { + workItemId: number +} + +export interface GetWorkItemResponse extends ToolResponse { + output: { + content: string + metadata: { + workItem: AzureDevOpsWorkItem + } + } +} + +// ── Get Work Items Batch ─────────────────────────────────────────────────────── + +export interface GetWorkItemsBatchParams extends AzureDevOpsBaseParams { + ids: string +} + +export interface GetWorkItemsBatchResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + workItems: AzureDevOpsWorkItem[] + } + } +} + +// ── Create Work Item ─────────────────────────────────────────────────────────── + +export type AzureDevOpsBasicWorkItemType = 'Issue' | 'Task' | 'Epic' + +export interface CreateWorkItemParams extends AzureDevOpsBaseParams { + workItemType: AzureDevOpsBasicWorkItemType + title: string + description?: string + assignedTo?: string + priority?: number + /** Microsoft.VSTS.Scheduling.Effort — Issue only in the Basic process. */ + effort?: number + /** Microsoft.VSTS.Scheduling.StartDate — Epic only in the Basic process. ISO 8601. */ + startDate?: string + /** Microsoft.VSTS.Scheduling.TargetDate — Epic only in the Basic process. ISO 8601. */ + targetDate?: string + /** Microsoft.VSTS.Common.Activity — Task only in the Basic process. */ + activity?: string + /** Microsoft.VSTS.Scheduling.RemainingWork — Task only in the Basic process. */ + remainingWork?: number + /** Microsoft.VSTS.Scheduling.CompletedWork — Task only in the Basic process. */ + completedWork?: number + areaPath?: string + iterationPath?: string + tags?: string +} + +export interface CreateWorkItemResponse extends ToolResponse { + output: { + content: string + metadata: { + workItem: AzureDevOpsWorkItem + } + } +} + +// ── Update Work Item ─────────────────────────────────────────────────────────── + +export interface UpdateWorkItemParams extends AzureDevOpsBaseParams { + workItemId: number + title?: string + description?: string + assignedTo?: string + priority?: number + /** Microsoft.VSTS.Scheduling.Effort — Issue only in the Basic process. */ + effort?: number + /** Microsoft.VSTS.Scheduling.StartDate — Epic only in the Basic process. ISO 8601. */ + startDate?: string + /** Microsoft.VSTS.Scheduling.TargetDate — Epic only in the Basic process. ISO 8601. */ + targetDate?: string + /** Microsoft.VSTS.Common.Activity — Task only in the Basic process. */ + activity?: string + /** Microsoft.VSTS.Scheduling.RemainingWork — Task only in the Basic process. */ + remainingWork?: number + /** Microsoft.VSTS.Scheduling.CompletedWork — Task only in the Basic process. */ + completedWork?: number + areaPath?: string + state?: string + tags?: string +} + +export interface UpdateWorkItemResponse extends ToolResponse { + output: { + content: string + metadata: { + workItem: AzureDevOpsWorkItem + } + } +} + +// ── Add Comment ──────────────────────────────────────────────────────────────── + +export interface AddCommentParams extends AzureDevOpsBaseParams { + workItemId: number + text: string +} + +export interface AzureDevOpsComment { + workItemId: number + commentId: number + version: number + text: string + renderedText?: string + createdBy: string | null + createdDate: string + modifiedBy: string | null + modifiedDate: string + isDeleted: boolean + url: string +} + +export interface AddCommentResponse extends ToolResponse { + output: { + content: string + metadata: { + comment: AzureDevOpsComment + } + } +} + +// ── Response Union ──────────────────────────────────────────────────────────── + +export type AzureDevOpsResponse = + | ListPipelinesResponse + | GetPipelineResponse + | ListPipelineRunsResponse + | GetPipelineRunResponse + | ListBuildsResponse + | ListBuildLogsResponse + | GetBuildLogResponse + | GetBuildTimelineResponse + | GetWorkItemsBetweenBuildsResponse + | QueryWorkItemsResponse + | GetWorkItemResponse + | GetWorkItemsBatchResponse + | CreateWorkItemResponse + | UpdateWorkItemResponse + | AddCommentResponse + | GetCommentsResponse + +// ── Get Comments ────────────────────────────────────────────────────────────── + +export interface GetCommentsParams extends AzureDevOpsBaseParams { + workItemId: number + top?: number + continuationToken?: string + includeDeleted?: boolean + expand?: string + order?: string +} + +export interface GetCommentsResponse extends ToolResponse { + output: { + content: string + metadata: { + count: number + totalCount: number + comments: AzureDevOpsComment[] + continuationToken?: string + nextPage?: string + url?: string + } + } +} diff --git a/apps/sim/tools/azure_devops/update_work_item.ts b/apps/sim/tools/azure_devops/update_work_item.ts new file mode 100644 index 00000000000..2d236d73b4c --- /dev/null +++ b/apps/sim/tools/azure_devops/update_work_item.ts @@ -0,0 +1,247 @@ +import type { + AzureDevOpsWorkItem, + UpdateWorkItemParams, + UpdateWorkItemResponse, +} from '@/tools/azure_devops/types' +import type { AzureDevOpsJsonPatchOp, AzureDevOpsRawWorkItem } from '@/tools/azure_devops/utils' +import { + appendEffortPatchOp, + appendFieldPatchOp, + formatWorkItem, + mapWorkItem, +} from '@/tools/azure_devops/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateWorkItemTool: ToolConfig = { + id: 'azure_devops_update_work_item', + name: 'Azure DevOps Update Work Item', + description: + 'Update one or more fields on an existing work item in Azure DevOps. Provide only the fields you want to change.', + version: '1.0.0', + + params: { + organization: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps organization name', + }, + project: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Azure DevOps project name', + }, + workItemId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'ID of the work item to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the work item (optional)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New HTML description for the work item (optional)', + }, + assignedTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email or display name to reassign the work item to (optional)', + }, + areaPath: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New area path for the work item (optional)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Priority of the work item (1 = Critical, 2 = High, 3 = Medium, 4 = Low) (optional)', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'New state for Basic-process work items: "To Do", "Doing", or "Done" (optional)', + }, + effort: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Effort (Microsoft.VSTS.Scheduling.Effort). Basic process: Issue only.', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Start date (Microsoft.VSTS.Scheduling.StartDate), ISO 8601. Basic process: Epic only.', + }, + targetDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Target date (Microsoft.VSTS.Scheduling.TargetDate), ISO 8601. Basic process: Epic only.', + }, + activity: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Activity (Microsoft.VSTS.Common.Activity). One of Deployment, Design, Development, Documentation, Requirements, Testing. Basic process: Task only.', + }, + remainingWork: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Remaining work hours (Microsoft.VSTS.Scheduling.RemainingWork). Basic process: Task only.', + }, + completedWork: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Completed work hours (Microsoft.VSTS.Scheduling.CompletedWork). Basic process: Task only.', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Semicolon-separated tags to set on the work item (optional)', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Azure DevOps Personal Access Token (scopes: Work Items: Read & Write)', + }, + }, + + request: { + url: (params) => + `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}?api-version=7.2-preview.3`, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json-patch+json', + Authorization: `Basic ${btoa(`:${params.accessToken}`)}`, + }), + body: (params) => { + const ops: AzureDevOpsJsonPatchOp[] = [] + if (params.title) { + ops.push({ op: 'replace', path: '/fields/System.Title', value: params.title }) + } + if (params.description) { + ops.push({ op: 'replace', path: '/fields/System.Description', value: params.description }) + } + if (params.assignedTo) { + ops.push({ op: 'replace', path: '/fields/System.AssignedTo', value: params.assignedTo }) + } + if (params.areaPath) { + ops.push({ op: 'replace', path: '/fields/System.AreaPath', value: params.areaPath }) + } + if (params.priority !== undefined) { + ops.push({ + op: 'replace', + path: '/fields/Microsoft.VSTS.Common.Priority', + value: String(Number(params.priority)), + }) + } + if (params.state) { + ops.push({ op: 'replace', path: '/fields/System.State', value: params.state }) + } + appendEffortPatchOp(ops, params.effort, 'replace') + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.StartDate', + params.startDate, + 'replace', + 'string' + ) + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.TargetDate', + params.targetDate, + 'replace', + 'string' + ) + appendFieldPatchOp(ops, 'Microsoft.VSTS.Common.Activity', params.activity, 'replace', 'string') + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.RemainingWork', + params.remainingWork, + 'replace', + 'number' + ) + appendFieldPatchOp( + ops, + 'Microsoft.VSTS.Scheduling.CompletedWork', + params.completedWork, + 'replace', + 'number' + ) + if (params.tags) { + ops.push({ op: 'replace', path: '/fields/System.Tags', value: params.tags }) + } + return ops + }, + }, + + transformResponse: async (response) => { + const raw: AzureDevOpsRawWorkItem = await response.json() + const workItem: AzureDevOpsWorkItem = mapWorkItem(raw) + return { + success: true, + output: { + content: `Updated work item #${workItem.id}:\n\n${formatWorkItem(workItem)}`, + metadata: { workItem }, + }, + } + }, + + outputs: { + content: { + type: 'string', + description: 'Human-readable summary of the updated work item', + }, + metadata: { + type: 'object', + description: 'Updated work item metadata', + properties: { + workItem: { + type: 'object', + description: 'Full details of the updated work item', + properties: { + id: { type: 'number', description: 'Work item ID' }, + title: { type: 'string', description: 'Work item title' }, + state: { type: 'string', description: 'Current state after update' }, + workItemType: { + type: 'string', + description: 'Work item type returned by Azure DevOps (e.g. Issue, Task, Epic)', + }, + assignedTo: { + type: 'string', + description: 'Display name of assigned user, or null if unassigned', + }, + areaPath: { type: 'string', description: 'Area path of the work item' }, + url: { type: 'string', description: 'API URL for the work item' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/azure_devops/utils.ts b/apps/sim/tools/azure_devops/utils.ts new file mode 100644 index 00000000000..b0955498554 --- /dev/null +++ b/apps/sim/tools/azure_devops/utils.ts @@ -0,0 +1,120 @@ +import type { AzureDevOpsComment, AzureDevOpsWorkItem } from '@/tools/azure_devops/types' + +/** States for Azure DevOps Basic process work items (Issue, Task, Epic). */ +export const AZURE_DEVOPS_BASIC_WORK_ITEM_STATES = ['To Do', 'Doing', 'Done'] as const + +/** Work item types for Azure DevOps Basic process. */ +export const AZURE_DEVOPS_BASIC_WORK_ITEM_TYPES = ['Issue', 'Task', 'Epic'] as const + +export type AzureDevOpsJsonPatchOp = { + op: string + path: string + value: string | number +} + +/** + * Appends a JSON-Patch op for a single work item field when the value is non-empty. + * Skips silently on undefined/empty-string. Numbers are validated; strings are + * passed through. + */ +export function appendFieldPatchOp( + ops: AzureDevOpsJsonPatchOp[], + refName: string, + value: string | number | undefined, + patchOp: 'add' | 'replace', + kind: 'number' | 'string' +): void { + if (value === undefined || value === '') return + if (kind === 'number') { + const numeric = Number(value) + if (Number.isNaN(numeric)) return + ops.push({ op: patchOp, path: `/fields/${refName}`, value: numeric }) + return + } + ops.push({ op: patchOp, path: `/fields/${refName}`, value: String(value) }) +} + +/** + * Appends a Microsoft.VSTS.Scheduling.Effort patch when effort is a valid number. + * Field availability depends on work item type and process template (Issue in Basic). + */ +export function appendEffortPatchOp( + ops: AzureDevOpsJsonPatchOp[], + effort: number | string | undefined, + patchOp: 'add' | 'replace' +): void { + appendFieldPatchOp(ops, 'Microsoft.VSTS.Scheduling.Effort', effort, patchOp, 'number') +} + +export function mapWorkItem(raw: AzureDevOpsRawWorkItem): AzureDevOpsWorkItem { + const fields = raw.fields ?? {} + return { + id: raw.id, + title: (fields['System.Title'] as string | undefined) ?? '', + state: (fields['System.State'] as string | undefined) ?? '', + workItemType: (fields['System.WorkItemType'] as string | undefined) ?? '', + assignedTo: + (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? null, + areaPath: (fields['System.AreaPath'] as string | undefined) ?? '', + url: raw.url, + } +} + +export function formatWorkItem(w: AzureDevOpsWorkItem): string { + return [ + `ID: ${w.id} [${w.workItemType}] ${w.title}`, + ` State: ${w.state}`, + ` Assigned To: ${w.assignedTo ?? 'Unassigned'}`, + ` Area: ${w.areaPath}`, + ].join('\n') +} + +export interface AzureDevOpsRawWorkItem { + id: number + url: string + fields: Record +} + +export function mapComment(raw: AzureDevOpsRawComment): AzureDevOpsComment { + return { + workItemId: raw.workItemId, + commentId: raw.commentId ?? raw.id, + version: raw.version, + text: raw.text, + renderedText: raw.renderedText, + createdBy: raw.createdBy?.displayName ?? null, + createdDate: raw.createdDate, + modifiedBy: raw.modifiedBy?.displayName ?? null, + modifiedDate: raw.modifiedDate, + isDeleted: raw.isDeleted ?? false, + url: raw.url, + } +} + +export function formatComment(comment: AzureDevOpsComment): string { + return [ + `Comment #${comment.commentId} on work item #${comment.workItemId}`, + ` Author: ${comment.createdBy ?? 'Unknown'}`, + ` Created: ${comment.createdDate}`, + ` Text: ${comment.text}`, + ].join('\n') +} + +interface AzureDevOpsIdentityRef { + displayName?: string +} + +export interface AzureDevOpsRawComment { + id: number + commentId?: number + workItemId: number + version: number + text: string + renderedText?: string + createdBy?: AzureDevOpsIdentityRef + createdDate: string + modifiedBy?: AzureDevOpsIdentityRef + modifiedDate: string + isDeleted?: boolean + url: string +} From 9c9e72d81de552f6afb7ad11e266881921009815 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:07:09 -0500 Subject: [PATCH 08/16] ADO workflow triggers --- .../lib/webhooks/providers/azure-devops.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 apps/sim/lib/webhooks/providers/azure-devops.ts diff --git a/apps/sim/lib/webhooks/providers/azure-devops.ts b/apps/sim/lib/webhooks/providers/azure-devops.ts new file mode 100644 index 00000000000..f72c0fdb203 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/azure-devops.ts @@ -0,0 +1,77 @@ +import { createLogger } from '@sim/logger' +import type { + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { + AZURE_DEVOPS_BUILD_FAILED_EVENT, + AZURE_DEVOPS_WORK_ITEM_CREATED_EVENT, + formatBuildCompleteInput, + formatWebhookEnvelopeInput, + formatWorkItemCreatedInput, +} from '@/triggers/azure_devops/utils' + +const logger = createLogger('WebhookProvider:AzureDevOps') + +export const azureDevOpsHandler: WebhookProviderHandler = { + async matchEvent({ + body, + requestId, + providerConfig, + webhook, + workflow, + }: EventMatchContext): Promise { + const triggerId = providerConfig.triggerId as string | undefined + const b = body as Record + + if (triggerId && triggerId !== 'azure_devops_webhook') { + const { isAzureDevOpsEventMatch } = await import('@/triggers/azure_devops/utils') + if (!isAzureDevOpsEventMatch(triggerId, b)) { + logger.debug( + `[${requestId}] Azure DevOps event mismatch for trigger ${triggerId}. Skipping execution.`, + { + webhookId: webhook.id, + workflowId: workflow.id, + triggerId, + eventType: b.eventType, + } + ) + return false + } + } + + return true + }, + + async formatInput({ body, webhook }: FormatInputContext): Promise { + const b = body as Record + const providerConfig = (webhook.providerConfig as Record) || {} + const triggerId = providerConfig.triggerId as string | undefined + const eventType = b.eventType as string | undefined + + if (triggerId === 'azure_devops_webhook') { + return { input: formatWebhookEnvelopeInput(b) } + } + + if (eventType === AZURE_DEVOPS_BUILD_FAILED_EVENT) { + return { input: formatBuildCompleteInput(b) } + } + + if (eventType === AZURE_DEVOPS_WORK_ITEM_CREATED_EVENT) { + return { input: formatWorkItemCreatedInput(b) } + } + + logger.warn('Azure DevOps: unknown eventType for specialized trigger', { + triggerId, + eventType, + }) + return { + input: null, + skip: { + message: `Unsupported Azure DevOps event type "${eventType ?? 'unknown'}" for trigger ${triggerId ?? 'unknown'}`, + }, + } + }, +} From 0edceafce3c1cc0f2979d0d0f9e24fac634f9435 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:08:23 -0500 Subject: [PATCH 09/16] block layer for ADO --- apps/sim/blocks/blocks/azure_devops.ts | 619 +++++++++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + 2 files changed, 621 insertions(+) create mode 100644 apps/sim/blocks/blocks/azure_devops.ts diff --git a/apps/sim/blocks/blocks/azure_devops.ts b/apps/sim/blocks/blocks/azure_devops.ts new file mode 100644 index 00000000000..b5113332107 --- /dev/null +++ b/apps/sim/blocks/blocks/azure_devops.ts @@ -0,0 +1,619 @@ +import { AzureDevOpsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { AzureDevOpsBasicWorkItemType, AzureDevOpsResponse } from '@/tools/azure_devops/types' +import { AZURE_DEVOPS_BASIC_WORK_ITEM_STATES } from '@/tools/azure_devops/utils' +import { getTrigger } from '@/triggers' + +/** Accepts ISO 8601 or YYYY-MM-DD; expands the bare date form to a UTC midnight ISO timestamp. */ +function normalizeDate(input: unknown): string | undefined { + if (typeof input !== 'string' || input.trim() === '') return undefined + const value = input.trim() + return /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T00:00:00Z` : value +} + +export const AzureDevOpsBlock: BlockConfig = { + type: 'azure_devops', + name: 'Azure DevOps', + description: 'Interact with Azure DevOps pipelines, builds, and work items', + longDescription: + 'Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.', + docsLink: 'https://docs.sim.ai/tools/azure_devops', + category: 'tools', + integrationType: IntegrationType.DeveloperTools, + tags: ['ci-cd', 'project-management', 'version-control'], + bgColor: '#FFFFFF', + icon: AzureDevOpsIcon, + authMode: AuthMode.ApiKey, + triggerAllowed: true, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Pipeline + { label: 'List Pipelines', id: 'azure_devops_list_pipelines' }, + { label: 'Get Pipeline', id: 'azure_devops_get_pipeline' }, + { label: 'List Pipeline Runs', id: 'azure_devops_list_pipeline_runs' }, + { label: 'Get Pipeline Run', id: 'azure_devops_get_pipeline_run' }, + // Builds + { label: 'List Builds', id: 'azure_devops_list_builds' }, + { label: 'List Build Logs', id: 'azure_devops_list_build_logs' }, + { label: 'Get Build Log', id: 'azure_devops_get_build_log' }, + { label: 'Get Build Timeline', id: 'azure_devops_get_build_timeline' }, + { + label: 'Get Work Items Between Builds', + id: 'azure_devops_get_work_items_between_builds', + }, + // Work Items + { label: 'Query Work Items', id: 'azure_devops_query_work_items' }, + { label: 'Get Work Item', id: 'azure_devops_get_work_item' }, + { label: 'Get Work Items Batch', id: 'azure_devops_get_work_items_batch' }, + { label: 'Create Work Item', id: 'azure_devops_create_work_item' }, + { label: 'Update Work Item', id: 'azure_devops_update_work_item' }, + { label: 'Add Comment', id: 'azure_devops_add_comment' }, + { label: 'Get Comments', id: 'azure_devops_get_comments' }, + ], + value: () => 'azure_devops_list_pipelines', + }, + + // ── Shared auth + org/project ──────────────────────────────────────────── + { + id: 'accessToken', + title: 'Personal Access Token', + type: 'short-input', + password: true, + required: true, + placeholder: 'Requires Build: Read and Work Items: Read & Write scopes', + }, + { + id: 'organization', + title: 'Organization', + type: 'short-input', + required: true, + placeholder: 'e.g. contoso', + }, + { + id: 'project', + title: 'Project', + type: 'short-input', + required: true, + placeholder: 'e.g. MyApp', + }, + + // ── Pipeline fields ────────────────────────────────────────────────────── + { + id: 'pipelineId', + title: 'Pipeline ID', + type: 'short-input', + required: true, + condition: { + field: 'operation', + value: [ + 'azure_devops_get_pipeline', + 'azure_devops_list_pipeline_runs', + 'azure_devops_get_pipeline_run', + ], + }, + }, + { + id: 'runId', + title: 'Run ID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'azure_devops_get_pipeline_run' }, + }, + + // ── Build fields ───────────────────────────────────────────────────────── + { + id: 'resultFilter', + title: 'Filter by Result', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Succeeded', id: 'succeeded' }, + { label: 'Failed', id: 'failed' }, + { label: 'Canceled', id: 'canceled' }, + { label: 'Partially Succeeded', id: 'partiallySucceeded' }, + ], + condition: { field: 'operation', value: 'azure_devops_list_builds' }, + mode: 'advanced', + }, + { + id: 'top', + title: 'Max Results', + type: 'short-input', + placeholder: '50', + condition: { field: 'operation', value: 'azure_devops_list_builds' }, + mode: 'advanced', + }, + { + id: 'buildId', + title: 'Build ID', + type: 'short-input', + required: true, + condition: { + field: 'operation', + value: ['azure_devops_list_build_logs', 'azure_devops_get_build_log', 'azure_devops_get_build_timeline'], + }, + }, + { + id: 'logId', + title: 'Log ID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'azure_devops_get_build_log' }, + }, + { + id: 'fromBuildId', + title: 'From Build ID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'azure_devops_get_work_items_between_builds' }, + }, + { + id: 'toBuildId', + title: 'To Build ID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'azure_devops_get_work_items_between_builds' }, + }, + + // ── Work Item fields ───────────────────────────────────────────────────── + { + id: 'wiqlQuery', + title: 'WIQL Query', + type: 'long-input', + required: true, + placeholder: + 'SELECT [System.Id], [System.Title], [System.State] FROM workitems WHERE [System.TeamProject] = @project ORDER BY [System.CreatedDate] DESC', + condition: { field: 'operation', value: 'azure_devops_query_work_items' }, + }, + { + id: 'workItemId', + title: 'Work Item ID', + type: 'short-input', + required: true, + condition: { + field: 'operation', + value: [ + 'azure_devops_get_work_item', + 'azure_devops_update_work_item', + 'azure_devops_add_comment', + 'azure_devops_get_comments', + ], + }, + }, + { + id: 'workItemIds', + title: 'Work Item IDs', + type: 'short-input', + required: true, + placeholder: 'Comma-separated IDs, e.g. 1,2,3', + condition: { field: 'operation', value: 'azure_devops_get_work_items_batch' }, + }, + { + id: 'workItemType', + title: 'Work Item Type', + type: 'dropdown', + required: true, + options: [ + { label: 'Issue', id: 'Issue' }, + { label: 'Task', id: 'Task' }, + { label: 'Epic', id: 'Epic' }, + ], + value: () => 'Issue', + condition: { field: 'operation', value: 'azure_devops_create_work_item' }, + }, + { + id: 'title', + title: 'Title', + type: 'short-input', + required: { field: 'operation', value: 'azure_devops_create_work_item' }, + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, + }, + { + id: 'assignedTo', + title: 'Assigned To', + type: 'short-input', + placeholder: 'Email or display name', + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, + mode: 'advanced', + }, + { + id: 'priority', + title: 'Priority', + type: 'dropdown', + options: [ + { label: '1 - Critical', id: '1' }, + { label: '2 - High', id: '2' }, + { label: '3 - Medium', id: '3' }, + { label: '4 - Low', id: '4' }, + ], + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, + mode: 'advanced', + }, + { + id: 'effort', + title: 'Effort', + type: 'short-input', + placeholder: 'Numeric effort (Issue only)', + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Issue' }, + }, + mode: 'advanced', + }, + { + id: 'effort', + title: 'Effort', + type: 'short-input', + placeholder: 'Numeric effort (Issue only)', + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (Epic only)', + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Epic' }, + }, + mode: 'advanced', + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (Epic only)', + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'targetDate', + title: 'Target Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (Epic only)', + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Epic' }, + }, + mode: 'advanced', + }, + { + id: 'targetDate', + title: 'Target Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (Epic only)', + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'activity', + title: 'Activity', + type: 'dropdown', + options: [ + { label: 'Deployment', id: 'Deployment' }, + { label: 'Design', id: 'Design' }, + { label: 'Development', id: 'Development' }, + { label: 'Documentation', id: 'Documentation' }, + { label: 'Requirements', id: 'Requirements' }, + { label: 'Testing', id: 'Testing' }, + ], + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Task' }, + }, + mode: 'advanced', + }, + { + id: 'activity', + title: 'Activity', + type: 'dropdown', + options: [ + { label: 'Deployment', id: 'Deployment' }, + { label: 'Design', id: 'Design' }, + { label: 'Development', id: 'Development' }, + { label: 'Documentation', id: 'Documentation' }, + { label: 'Requirements', id: 'Requirements' }, + { label: 'Testing', id: 'Testing' }, + ], + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'remainingWork', + title: 'Remaining Work', + type: 'short-input', + placeholder: 'Hours (Task only)', + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Task' }, + }, + mode: 'advanced', + }, + { + id: 'remainingWork', + title: 'Remaining Work', + type: 'short-input', + placeholder: 'Hours (Task only)', + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'completedWork', + title: 'Completed Work', + type: 'short-input', + placeholder: 'Hours (Task only)', + condition: { + field: 'operation', + value: 'azure_devops_create_work_item', + and: { field: 'workItemType', value: 'Task' }, + }, + mode: 'advanced', + }, + { + id: 'completedWork', + title: 'Completed Work', + type: 'short-input', + placeholder: 'Hours (Task only)', + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + mode: 'advanced', + }, + { + id: 'areaPath', + title: 'Area Path', + type: 'short-input', + placeholder: 'e.g. MyProject\\Team', + condition: { field: 'operation', value: 'azure_devops_create_work_item' }, + mode: 'advanced', + }, + { + id: 'iterationPath', + title: 'Iteration Path', + type: 'short-input', + placeholder: 'e.g. MyProject\\Sprint 1', + condition: { field: 'operation', value: 'azure_devops_create_work_item' }, + mode: 'advanced', + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'Semicolon-separated, e.g. issue; p1; auth', + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, + mode: 'advanced', + }, + { + id: 'state', + title: 'State', + type: 'dropdown', + options: AZURE_DEVOPS_BASIC_WORK_ITEM_STATES.map((state) => ({ + label: state, + id: state, + })), + condition: { field: 'operation', value: 'azure_devops_update_work_item' }, + }, + { + id: 'commentText', + title: 'Comment', + type: 'long-input', + required: true, + condition: { field: 'operation', value: 'azure_devops_add_comment' }, + }, + ...getTrigger('azure_devops_build_failed').subBlocks, + ...getTrigger('azure_devops_work_item_created').subBlocks, + ...getTrigger('azure_devops_webhook').subBlocks, + ], + + tools: { + access: [ + 'azure_devops_list_pipelines', + 'azure_devops_get_pipeline', + 'azure_devops_list_pipeline_runs', + 'azure_devops_get_pipeline_run', + 'azure_devops_list_builds', + 'azure_devops_list_build_logs', + 'azure_devops_get_build_log', + 'azure_devops_get_build_timeline', + 'azure_devops_get_work_items_between_builds', + 'azure_devops_query_work_items', + 'azure_devops_get_work_item', + 'azure_devops_get_work_items_batch', + 'azure_devops_create_work_item', + 'azure_devops_update_work_item', + 'azure_devops_add_comment', + 'azure_devops_get_comments', + ], + config: { + tool: (params) => params.operation as string, + params: (params) => { + const base = { + accessToken: params.accessToken as string, + organization: params.organization as string, + project: params.project as string, + } + switch (params.operation) { + case 'azure_devops_list_pipelines': + return base + case 'azure_devops_get_pipeline': + return { ...base, pipelineId: Number(params.pipelineId) } + case 'azure_devops_list_pipeline_runs': + return { ...base, pipelineId: Number(params.pipelineId) } + case 'azure_devops_get_pipeline_run': + return { ...base, pipelineId: Number(params.pipelineId), runId: Number(params.runId) } + case 'azure_devops_list_builds': + return { + ...base, + resultFilter: (params.resultFilter as string) || undefined, + top: params.top ? Number(params.top) : undefined, + } + case 'azure_devops_list_build_logs': + return { ...base, buildId: Number(params.buildId) } + case 'azure_devops_get_build_log': + return { ...base, buildId: Number(params.buildId), logId: Number(params.logId) } + case 'azure_devops_get_build_timeline': + return { ...base, buildId: Number(params.buildId) } + case 'azure_devops_get_work_items_between_builds': + return { + ...base, + fromBuildId: Number(params.fromBuildId), + toBuildId: Number(params.toBuildId), + } + case 'azure_devops_query_work_items': + return { ...base, wiqlQuery: params.wiqlQuery as string } + case 'azure_devops_get_work_item': + return { ...base, workItemId: Number(params.workItemId) } + case 'azure_devops_get_work_items_batch': + return { ...base, ids: params.workItemIds as string } + case 'azure_devops_create_work_item': + return { + ...base, + workItemType: params.workItemType as AzureDevOpsBasicWorkItemType, + title: params.title as string, + description: (params.description as string) || undefined, + assignedTo: (params.assignedTo as string) || undefined, + priority: params.priority ? Number(params.priority) : undefined, + effort: params.effort ? Number(params.effort) : undefined, + startDate: normalizeDate(params.startDate), + targetDate: normalizeDate(params.targetDate), + activity: (params.activity as string) || undefined, + remainingWork: params.remainingWork ? Number(params.remainingWork) : undefined, + completedWork: params.completedWork ? Number(params.completedWork) : undefined, + areaPath: (params.areaPath as string) || undefined, + iterationPath: (params.iterationPath as string) || undefined, + tags: (params.tags as string) || undefined, + } + case 'azure_devops_update_work_item': + return { + ...base, + workItemId: Number(params.workItemId), + title: (params.title as string) || undefined, + state: (params.state as string) || undefined, + assignedTo: (params.assignedTo as string) || undefined, + priority: params.priority ? Number(params.priority) : undefined, + effort: params.effort ? Number(params.effort) : undefined, + startDate: normalizeDate(params.startDate), + targetDate: normalizeDate(params.targetDate), + activity: (params.activity as string) || undefined, + remainingWork: params.remainingWork ? Number(params.remainingWork) : undefined, + completedWork: params.completedWork ? Number(params.completedWork) : undefined, + description: (params.description as string) || undefined, + tags: (params.tags as string) || undefined, + } + case 'azure_devops_add_comment': + return { + ...base, + workItemId: Number(params.workItemId), + text: params.commentText as string, + } + case 'azure_devops_get_comments': + return { ...base, workItemId: Number(params.workItemId) } + default: + return base + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + accessToken: { type: 'string', description: 'Azure DevOps Personal Access Token' }, + organization: { type: 'string', description: 'Azure DevOps organization name' }, + project: { type: 'string', description: 'Azure DevOps project name' }, + pipelineId: { type: 'number', description: 'Pipeline ID' }, + runId: { type: 'number', description: 'Pipeline run ID' }, + resultFilter: { type: 'string', description: 'Build result filter' }, + top: { type: 'number', description: 'Maximum number of results' }, + buildId: { type: 'number', description: 'Build ID' }, + logId: { type: 'number', description: 'Build log ID' }, + fromBuildId: { type: 'number', description: 'Starting build ID for work item range' }, + toBuildId: { type: 'number', description: 'Ending build ID for work item range' }, + wiqlQuery: { type: 'string', description: 'WIQL query string' }, + workItemId: { type: 'number', description: 'Work item ID' }, + workItemIds: { type: 'string', description: 'Comma-separated work item IDs' }, + workItemType: { type: 'string', description: 'Basic work item type (Issue, Task, Epic)' }, + title: { type: 'string', description: 'Work item title' }, + description: { type: 'string', description: 'Work item description (HTML supported)' }, + assignedTo: { type: 'string', description: 'Assignee email or display name' }, + priority: { type: 'number', description: 'Work item priority (1–4)' }, + effort: { + type: 'number', + description: 'Work item effort (Microsoft.VSTS.Scheduling.Effort); Basic process: Issue only', + }, + startDate: { + type: 'string', + description: 'Start date (Microsoft.VSTS.Scheduling.StartDate); Basic process: Epic only', + }, + targetDate: { + type: 'string', + description: 'Target date (Microsoft.VSTS.Scheduling.TargetDate); Basic process: Epic only', + }, + activity: { + type: 'string', + description: 'Activity (Microsoft.VSTS.Common.Activity); Basic process: Task only', + }, + remainingWork: { + type: 'number', + description: + 'Remaining work hours (Microsoft.VSTS.Scheduling.RemainingWork); Basic process: Task only', + }, + completedWork: { + type: 'number', + description: + 'Completed work hours (Microsoft.VSTS.Scheduling.CompletedWork); Basic process: Task only', + }, + areaPath: { type: 'string', description: 'Area path' }, + iterationPath: { type: 'string', description: 'Iteration path' }, + tags: { type: 'string', description: 'Semicolon-separated tags' }, + state: { + type: 'string', + description: 'Basic-process work item state (To Do, Doing, Done)', + }, + commentText: { type: 'string', description: 'Comment text' }, + }, + + outputs: { + content: { type: 'string', description: 'Human-readable response from Azure DevOps' }, + metadata: { type: 'json', description: 'Structured Azure DevOps response data' }, + }, + + triggers: { + enabled: true, + available: [ + 'azure_devops_build_failed', + 'azure_devops_work_item_created', + 'azure_devops_webhook', + ], + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index d458289879a..e9bef334ba1 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -17,6 +17,7 @@ import { AsanaBlock } from '@/blocks/blocks/asana' import { AshbyBlock } from '@/blocks/blocks/ashby' import { AthenaBlock } from '@/blocks/blocks/athena' import { AttioBlock } from '@/blocks/blocks/attio' +import { AzureDevOpsBlock } from '@/blocks/blocks/azure_devops' import { BoxBlock } from '@/blocks/blocks/box' import { BrandfetchBlock } from '@/blocks/blocks/brandfetch' import { BrightDataBlock } from '@/blocks/blocks/brightdata' @@ -255,6 +256,7 @@ export const registry: Record = { ashby: AshbyBlock, athena: AthenaBlock, attio: AttioBlock, + azure_devops: AzureDevOpsBlock, box: BoxBlock, brandfetch: BrandfetchBlock, brightdata: BrightDataBlock, From 0432086d1e271b5f0cb7260bc6f6de7f9ec7bebe Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:08:58 -0500 Subject: [PATCH 10/16] ADO icon svg --- apps/sim/components/icons.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 95502bf3ff6..382d2d2823c 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3048,6 +3048,36 @@ export function AzureIcon(props: SVGProps) { ) } +export function AzureDevOpsIcon(props: SVGProps) { + const id = useId() + const gradientId = `azure_devops_gradient_${id}` + return ( + + + + + + + + + + + + + ) +} + export const GroqIcon = (props: SVGProps) => ( Date: Sat, 16 May 2026 23:09:58 -0500 Subject: [PATCH 11/16] generated docs for ADO triggers --- .../content/docs/en/triggers/azure_devops.mdx | 83 +++++++++++++++++++ apps/docs/content/docs/en/triggers/meta.json | 1 + 2 files changed, 84 insertions(+) create mode 100644 apps/docs/content/docs/en/triggers/azure_devops.mdx diff --git a/apps/docs/content/docs/en/triggers/azure_devops.mdx b/apps/docs/content/docs/en/triggers/azure_devops.mdx new file mode 100644 index 00000000000..c329d55b2ea --- /dev/null +++ b/apps/docs/content/docs/en/triggers/azure_devops.mdx @@ -0,0 +1,83 @@ +--- +title: Azure Devops +description: Available Azure Devops triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Azure Devops provides 3 triggers for automating workflows based on events. + +## Triggers + +### Azure DevOps Build Failed + +Trigger workflow when an Azure DevOps build fails, is canceled, or partially succeeds + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `buildId` | number | Build ID | +| `buildNumber` | string | Build number string \(e.g. 20240101.1\) | +| `result` | string | Build result: failed \| canceled \| partiallySucceeded | +| `pipelineId` | number | Pipeline definition ID | +| `pipelineName` | string | Pipeline definition name | +| `projectName` | string | Azure DevOps project name | +| `branch` | string | Source branch name \(refs/heads/ prefix stripped\) | +| `commitSha` | string | Source commit SHA | +| `triggeredBy` | string | Display name of the person who triggered the build | +| `triggeredByEmail` | string | Email/unique name of the person who triggered the build | +| `startTime` | string | Build start time \(ISO 8601\) | +| `finishTime` | string | Build finish time \(ISO 8601\) | +| `buildUrl` | string | API URL for the build resource | + + +--- + +### Azure DevOps Webhook (All Service Hook Events) + +Trigger on whichever service hook event types you configure in Azure DevOps. Sim does not filter deliveries for this trigger. + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Service hook event type \(e.g. build.complete, workitem.created\) | +| `notificationId` | number | Notification ID | +| `subscriptionId` | string | Service hook subscription ID | +| `publisherId` | string | Publisher ID \(e.g. tfs\) | +| `createdDate` | string | Event creation time \(ISO 8601\) | +| `resource` | json | Event resource payload | +| `resourceContainers` | json | Resource container references \(project, collection, etc.\) | +| `message` | json | Short message object | +| `detailedMessage` | json | Detailed message object | + + +--- + +### Azure DevOps Work Item Created + +Trigger workflow when a work item is created in Azure DevOps + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `workItemId` | number | Work item ID | +| `workItemType` | string | Work item type for Basic process \(e.g. Issue, Task, Epic\) | +| `title` | string | Work item title | +| `state` | string | Work item state for Basic process \(e.g. To Do, Doing, Done\) | +| `createdBy` | string | Display name of the creator | +| `assignedTo` | string | Assignee display name, or empty string if unassigned | +| `priority` | number | Priority \(1–4\), or 0 if not set | +| `areaPath` | string | Area path | +| `iterationPath` | string | Iteration path | +| `description` | string | Work item description \(HTML\), or empty string if not set | +| `projectName` | string | Azure DevOps project name | +| `workItemUrl` | string | API URL for the work item resource | + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index 70d13afd920..ab14483dd52 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -8,6 +8,7 @@ "airtable", "ashby", "attio", + "azure_devops", "calcom", "calendly", "circleback", From 4aeed9f012dd356473711c98839284804f4b0937 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sun, 17 May 2026 01:43:39 -0500 Subject: [PATCH 12/16] committing the tests for azure devops tools and blocks --- apps/sim/blocks/blocks/azure_devops.test.ts | 162 +++++ .../tools/azure_devops/azure-devops.test.ts | 652 ++++++++++++++++++ 2 files changed, 814 insertions(+) create mode 100644 apps/sim/blocks/blocks/azure_devops.test.ts create mode 100644 apps/sim/tools/azure_devops/azure-devops.test.ts diff --git a/apps/sim/blocks/blocks/azure_devops.test.ts b/apps/sim/blocks/blocks/azure_devops.test.ts new file mode 100644 index 00000000000..8180f6d60e2 --- /dev/null +++ b/apps/sim/blocks/blocks/azure_devops.test.ts @@ -0,0 +1,162 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { AzureDevOpsBlock } from './azure_devops' + +const expectedToolIds = [ + 'azure_devops_add_comment', + 'azure_devops_create_work_item', + 'azure_devops_get_build_log', + 'azure_devops_get_build_timeline', + 'azure_devops_get_comments', + 'azure_devops_get_pipeline', + 'azure_devops_get_pipeline_run', + 'azure_devops_get_work_item', + 'azure_devops_get_work_items_batch', + 'azure_devops_get_work_items_between_builds', + 'azure_devops_list_build_logs', + 'azure_devops_list_builds', + 'azure_devops_list_pipeline_runs', + 'azure_devops_list_pipelines', + 'azure_devops_query_work_items', + 'azure_devops_update_work_item', +] + +describe('AzureDevOpsBlock', () => { + const block = AzureDevOpsBlock + + it('exposes every Azure DevOps tool through the operation dropdown and tool access list', () => { + const operation = block.subBlocks.find((subBlock) => subBlock.id === 'operation') + expect(operation?.type).toBe('dropdown') + expect(block.tools.access.sort()).toEqual(expectedToolIds) + const operationOptions = + typeof operation?.options === 'function' ? operation.options() : operation?.options + expect(operationOptions?.map((option) => option.id).sort()).toEqual(expectedToolIds) + }) + + it('limits update work item state to Azure DevOps Basic process options', () => { + const state = block.subBlocks.find((subBlock) => subBlock.id === 'state') + expect(state?.type).toBe('dropdown') + expect(state?.options).toEqual([ + { label: 'To Do', id: 'To Do' }, + { label: 'Doing', id: 'Doing' }, + { label: 'Done', id: 'Done' }, + ]) + }) + + it('limits create work item types to the Azure DevOps Basic process options', () => { + const workItemType = block.subBlocks.find((subBlock) => subBlock.id === 'workItemType') + expect(workItemType?.type).toBe('dropdown') + expect(workItemType?.options).toEqual([ + { label: 'Issue', id: 'Issue' }, + { label: 'Task', id: 'Task' }, + { label: 'Epic', id: 'Epic' }, + ]) + expect(workItemType?.value?.()).toBe('Issue') + }) + + it('routes every operation to the matching tool id without serialization-time coercion', () => { + for (const toolId of expectedToolIds) { + expect(block.tools.config.tool?.({ operation: toolId })).toBe(toolId) + } + }) + + it('maps common params and coerces numeric fields at execution time', () => { + const pipelineRunParams = block.tools.config.params?.({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + operation: 'azure_devops_get_pipeline_run', + pipelineId: '42', + runId: '99', + }) + + expect(pipelineRunParams).toMatchObject({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + pipelineId: 42, + runId: 99, + }) + + const listBuildParams = block.tools.config.params?.({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + operation: 'azure_devops_list_builds', + resultFilter: 'failed', + top: '10', + }) + + expect(listBuildParams).toMatchObject({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + resultFilter: 'failed', + top: 10, + }) + + const getBuildLogParams = block.tools.config.params?.({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + operation: 'azure_devops_get_build_log', + buildId: '101', + logId: '3', + }) + + expect(getBuildLogParams).toMatchObject({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + buildId: 101, + logId: 3, + }) + + const createWorkItemParams = block.tools.config.params?.({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + operation: 'azure_devops_create_work_item', + workItemType: 'Issue', + title: 'Pipeline failure', + priority: '2', + }) + + expect(createWorkItemParams).toMatchObject({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + workItemType: 'Issue', + title: 'Pipeline failure', + priority: 2, + }) + + const updateWorkItemParams = block.tools.config.params?.({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + operation: 'azure_devops_update_work_item', + workItemId: '101', + state: 'Doing', + effort: '8', + priority: '1', + }) + + expect(updateWorkItemParams).toMatchObject({ + accessToken: 'pat-token', + organization: 'mzxchandra', + project: 'sim-testing', + workItemId: 101, + state: 'Doing', + effort: 8, + priority: 1, + }) + }) + + it('declares downstream outputs for pipeline, build, work item, and comment operations', () => { + expect(block.outputs.content).toBeDefined() + expect(block.outputs.metadata).toBeDefined() + }) +}) diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts new file mode 100644 index 00000000000..dd6ec4dfbc2 --- /dev/null +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -0,0 +1,652 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { tools } from '../registry' +import type { ToolConfig } from '../types' +import { addCommentTool } from './add_comment' +import { createWorkItemTool } from './create_work_item' +import { getBuildLogTool } from './get_build_log' +import { getCommentsTool } from './get_comments' +import { getPipelineTool } from './get_pipeline' +import { getPipelineRunTool } from './get_pipeline_run' +import { getWorkItemTool } from './get_work_item' +import { getWorkItemsBatchTool } from './get_work_items_batch' +import { getWorkItemsBetweenBuildsTool } from './get_work_items_between_builds' +import { listBuildLogsTool } from './list_build_logs' +import { listBuildsTool } from './list_builds' +import { listPipelineRunsTool } from './list_pipeline_runs' +import { listPipelinesTool } from './list_pipelines' +import { queryWorkItemsTool } from './query_work_items' +import type { + AddCommentParams, + CreateWorkItemParams, + GetBuildLogParams, + GetCommentsParams, + GetPipelineParams, + GetPipelineRunParams, + GetWorkItemParams, + GetWorkItemsBatchParams, + GetWorkItemsBetweenBuildsParams, + ListBuildLogsParams, + ListBuildsParams, + ListPipelineRunsParams, + ListPipelinesParams, + QueryWorkItemsParams, + UpdateWorkItemParams, +} from './types' +import { updateWorkItemTool } from './update_work_item' + +const baseParams = { + organization: 'contoso', + project: 'Fabrikam', + accessToken: 'pat-token', +} + +const authHeader = `Basic ${Buffer.from(':pat-token').toString('base64')}` + +const allTools = [ + addCommentTool, + createWorkItemTool, + getBuildLogTool, + getCommentsTool, + getPipelineTool, + getPipelineRunTool, + getWorkItemTool, + getWorkItemsBatchTool, + getWorkItemsBetweenBuildsTool, + listBuildLogsTool, + listBuildsTool, + listPipelineRunsTool, + listPipelinesTool, + queryWorkItemsTool, + updateWorkItemTool, +] as const + +function buildUrl(tool: ToolConfig, params: P): string { + return typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url +} + +function buildHeaders(tool: ToolConfig, params: P): Record { + return tool.request.headers(params) +} + +function buildBody(tool: ToolConfig, params: P): unknown { + return tool.request.body?.(params) +} + +function responseJson(body: unknown): Response { + return new Response(JSON.stringify(body)) +} + +const rawWorkItem = { + id: 101, + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101', + fields: { + 'System.Title': 'SimIntegrationTest Issue', + 'System.State': 'Doing', + 'System.WorkItemType': 'Issue', + 'System.AssignedTo': { displayName: 'Ada Lovelace' }, + 'System.AreaPath': 'Fabrikam\\Platform', + }, +} + +const rawComment = { + workItemId: 101, + commentId: 9, + version: 1, + text: 'SimIntegrationTest comment', + renderedText: '

SimIntegrationTest comment

', + createdBy: { displayName: 'Ada Lovelace' }, + createdDate: '2026-05-15T10:00:00Z', + modifiedBy: { displayName: 'Ada Lovelace' }, + modifiedDate: '2026-05-15T10:00:00Z', + isDeleted: false, + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments/9', + id: 9, +} + +describe('Azure DevOps tool contracts', () => { + it('exports and registers the full planned tool surface', () => { + const expectedIds = [ + 'azure_devops_add_comment', + 'azure_devops_create_work_item', + 'azure_devops_get_build_log', + 'azure_devops_get_comments', + 'azure_devops_get_pipeline', + 'azure_devops_get_pipeline_run', + 'azure_devops_get_work_item', + 'azure_devops_get_work_items_batch', + 'azure_devops_get_work_items_between_builds', + 'azure_devops_list_build_logs', + 'azure_devops_list_builds', + 'azure_devops_list_pipeline_runs', + 'azure_devops_list_pipelines', + 'azure_devops_query_work_items', + 'azure_devops_update_work_item', + ] + + expect(allTools.map((tool) => tool.id).sort()).toEqual(expectedIds) + for (const id of expectedIds) { + expect(tools[id]?.id).toBe(id) + } + }) + + it('sets Basic PAT auth on every tool', () => { + for (const tool of allTools) { + expect( + buildHeaders(tool, { + ...baseParams, + pipelineId: 1, + runId: 2, + buildId: 3, + logId: 4, + fromBuildId: 5, + toBuildId: 6, + workItemId: 7, + ids: '7', + wiqlQuery: 'SELECT [System.Id] FROM workitems', + workItemType: 'Issue', + title: 'Issue title', + text: 'Comment text', + }).Authorization + ).toBe(authHeader) + } + }) +}) + +describe('Azure DevOps request builders', () => { + it('builds pipeline URLs and optional params', () => { + expect(buildUrl(listPipelinesTool, baseParams)).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/pipelines?api-version=7.2-preview.1' + ) + expect( + buildUrl(listPipelinesTool, { + ...baseParams, + orderBy: 'name', + top: 10, + continuationToken: 'next-page', + } satisfies ListPipelinesParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/pipelines?api-version=7.2-preview.1&orderBy=name&%24top=10&continuationToken=next-page' + ) + expect( + buildUrl(getPipelineTool, { + ...baseParams, + pipelineId: 42, + pipelineVersion: 3, + } satisfies GetPipelineParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/pipelines/42?api-version=7.2-preview.1&pipelineVersion=3' + ) + expect( + buildUrl(listPipelineRunsTool, { + ...baseParams, + pipelineId: 42, + } satisfies ListPipelineRunsParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/pipelines/42/runs?api-version=7.2-preview.1' + ) + expect( + buildUrl(getPipelineRunTool, { + ...baseParams, + pipelineId: 42, + runId: 99, + } satisfies GetPipelineRunParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/pipelines/42/runs/99?api-version=7.2-preview.1' + ) + }) + + it('builds build URLs and optional filters', () => { + expect( + buildUrl(listBuildsTool, { + ...baseParams, + definitionIds: '1,2', + top: 20, + statusFilter: 'completed', + resultFilter: 'failed', + branchName: 'refs/heads/main', + } satisfies ListBuildsParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/build/builds?api-version=7.2-preview.8&definitions=1%2C2&%24top=20&statusFilter=completed&resultFilter=failed&branchName=refs%2Fheads%2Fmain' + ) + expect( + buildUrl(listBuildLogsTool, { + ...baseParams, + buildId: 101, + } satisfies ListBuildLogsParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/build/builds/101/logs?api-version=7.2-preview.2' + ) + expect( + buildUrl(getBuildLogTool, { + ...baseParams, + buildId: 101, + logId: 3, + startLine: 5, + endLine: 15, + } satisfies GetBuildLogParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/build/builds/101/logs/3?api-version=7.2-preview.2&startLine=5&endLine=15' + ) + expect(buildHeaders(getBuildLogTool, { ...baseParams, buildId: 101, logId: 3 }).Accept).toBe( + 'text/plain' + ) + }) + + it('uses the documented work-items-between-builds endpoint shape', () => { + expect( + buildUrl(getWorkItemsBetweenBuildsTool, { + ...baseParams, + fromBuildId: 11, + toBuildId: 12, + } satisfies GetWorkItemsBetweenBuildsParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/build/workitems?fromBuildId=11&toBuildId=12&api-version=7.2-preview.2' + ) + }) + + it('builds work item URLs and bodies', () => { + expect(buildUrl(queryWorkItemsTool, baseParams)).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/wiql?api-version=7.2-preview.2' + ) + expect( + buildBody(queryWorkItemsTool, { + ...baseParams, + wiqlQuery: 'SELECT [System.Id] FROM workitems', + } satisfies QueryWorkItemsParams) + ).toEqual({ query: 'SELECT [System.Id] FROM workitems' }) + expect( + buildUrl(getWorkItemTool, { + ...baseParams, + workItemId: 101, + } satisfies GetWorkItemParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101?%24expand=all&api-version=7.2-preview.3' + ) + expect( + buildUrl(getWorkItemsBatchTool, { + ...baseParams, + ids: '101,102', + } satisfies GetWorkItemsBatchParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems?ids=101%2C102&%24expand=all&api-version=7.2-preview.3' + ) + }) + + it('builds JSON Patch work item write requests', () => { + const createParams = { + ...baseParams, + workItemType: 'Issue', + title: 'Pipeline failure', + description: '

Failure details

', + assignedTo: 'ada@example.com', + areaPath: 'Fabrikam\\Platform', + } satisfies CreateWorkItemParams + + expect(buildUrl(createWorkItemTool, createParams)).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/$Issue?api-version=7.2-preview.3' + ) + expect(buildHeaders(createWorkItemTool, createParams)['Content-Type']).toBe( + 'application/json-patch+json' + ) + expect(buildBody(createWorkItemTool, createParams)).toEqual([ + { op: 'add', path: '/fields/System.Title', value: 'Pipeline failure' }, + { op: 'add', path: '/fields/System.Description', value: '

Failure details

' }, + { op: 'add', path: '/fields/System.AssignedTo', value: 'ada@example.com' }, + { op: 'add', path: '/fields/System.AreaPath', value: 'Fabrikam\\Platform' }, + ]) + + const updateParams = { + ...baseParams, + workItemId: 101, + title: 'Updated pipeline failure', + state: 'Doing', + effort: 5, + } satisfies UpdateWorkItemParams + + expect(buildUrl(updateWorkItemTool, updateParams)).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101?api-version=7.2-preview.3' + ) + expect(buildHeaders(updateWorkItemTool, updateParams)['Content-Type']).toBe( + 'application/json-patch+json' + ) + expect(buildBody(updateWorkItemTool, updateParams)).toEqual([ + { op: 'replace', path: '/fields/System.Title', value: 'Updated pipeline failure' }, + { op: 'replace', path: '/fields/System.State', value: 'Doing' }, + { op: 'replace', path: '/fields/Microsoft.VSTS.Scheduling.Effort', value: 5 }, + ]) + expect(buildBody(updateWorkItemTool, { ...baseParams, workItemId: 101 })).toEqual([]) + + const createWithEffortParams = { + ...createParams, + effort: 3, + } satisfies CreateWorkItemParams + + expect(buildBody(createWorkItemTool, createWithEffortParams)).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Scheduling.Effort', + value: 3, + }) + }) + + it('emits Epic-only scheduling patch ops on create', () => { + const epicParams = { + ...baseParams, + workItemType: 'Epic', + title: 'Q3 platform epic', + startDate: '2026-06-01T00:00:00Z', + targetDate: '2026-09-30T00:00:00Z', + } satisfies CreateWorkItemParams + + const body = buildBody(createWorkItemTool, epicParams) + expect(body).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Scheduling.StartDate', + value: '2026-06-01T00:00:00Z', + }) + expect(body).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Scheduling.TargetDate', + value: '2026-09-30T00:00:00Z', + }) + }) + + it('emits Task-only activity/work patch ops on create', () => { + const taskParams = { + ...baseParams, + workItemType: 'Task', + title: 'Wire up retries', + activity: 'Development', + remainingWork: 4, + completedWork: 1, + } satisfies CreateWorkItemParams + + const body = buildBody(createWorkItemTool, taskParams) + expect(body).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Common.Activity', + value: 'Development', + }) + expect(body).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Scheduling.RemainingWork', + value: 4, + }) + expect(body).toContainEqual({ + op: 'add', + path: '/fields/Microsoft.VSTS.Scheduling.CompletedWork', + value: 1, + }) + }) + + it('emits per-type replace ops on update when fields are provided', () => { + const updateAll = { + ...baseParams, + workItemId: 101, + startDate: '2026-06-01T00:00:00Z', + activity: 'Testing', + remainingWork: 2, + } satisfies UpdateWorkItemParams + + const body = buildBody(updateWorkItemTool, updateAll) + expect(body).toContainEqual({ + op: 'replace', + path: '/fields/Microsoft.VSTS.Scheduling.StartDate', + value: '2026-06-01T00:00:00Z', + }) + expect(body).toContainEqual({ + op: 'replace', + path: '/fields/Microsoft.VSTS.Common.Activity', + value: 'Testing', + }) + expect(body).toContainEqual({ + op: 'replace', + path: '/fields/Microsoft.VSTS.Scheduling.RemainingWork', + value: 2, + }) + }) + + it('builds comment URLs and bodies with comment API pinning', () => { + const addParams = { + ...baseParams, + workItemId: 101, + text: 'SimIntegrationTest markdown comment', + } satisfies AddCommentParams + + expect(buildUrl(addCommentTool, addParams)).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments?api-version=7.2-preview.4' + ) + expect(buildBody(addCommentTool, addParams)).toEqual({ + text: 'SimIntegrationTest markdown comment', + }) + + expect( + buildUrl(getCommentsTool, { + ...baseParams, + workItemId: 101, + top: 2, + continuationToken: 'next', + includeDeleted: true, + expand: 'renderedText', + order: 'desc', + } satisfies GetCommentsParams) + ).toBe( + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments?api-version=7.2-preview.4&%24top=2&continuationToken=next&includeDeleted=true&%24expand=renderedText&order=desc' + ) + }) +}) + +describe('Azure DevOps response transforms', () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + it('transforms list pipelines responses and empty results', async () => { + await expect( + listPipelinesTool.transformResponse!(responseJson({ count: 0, value: [] })) + ).resolves.toEqual({ + success: true, + output: { content: 'No pipelines found.', metadata: { count: 0, pipelines: [] } }, + }) + + const result = await listPipelinesTool.transformResponse!( + responseJson({ + value: [{ id: 1, name: 'CI', revision: 2, url: 'https://example/p/1' }], + }) + ) + + expect(result.output.metadata).toEqual({ + count: 1, + pipelines: [{ id: 1, name: 'CI', folder: '\\', revision: 2, url: 'https://example/p/1' }], + }) + }) + + it('transforms pipeline detail and run responses with missing optional links', async () => { + const pipeline = await getPipelineTool.transformResponse!( + responseJson({ + id: 42, + name: 'CI', + revision: 3, + url: 'https://example/p/42', + configuration: { type: 'yaml', path: '/azure-pipelines.yml' }, + }) + ) + + expect(pipeline.output.metadata.pipeline.links.web).toBe('') + expect(pipeline.output.metadata.pipeline.configuration.repository).toBeUndefined() + + const runs = await listPipelineRunsTool.transformResponse!(responseJson({ value: [] })) + expect(runs.output).toEqual({ + content: 'No pipeline runs found.', + metadata: { count: 0, runs: [] }, + }) + + const run = await getPipelineRunTool.transformResponse!( + responseJson({ + id: 99, + name: '20260515.1', + state: 'completed', + result: 'failed', + createdDate: '2026-05-15T10:00:00Z', + finishedDate: '2026-05-15T10:05:00Z', + url: 'https://example/r/99', + pipeline: { id: 42, name: 'CI', revision: 3, url: 'https://example/p/42' }, + }) + ) + + expect(run.output.metadata.run.pipeline.folder).toBe('\\') + expect(run.output.metadata.run.result).toBe('failed') + }) + + it('transforms build and log responses', async () => { + const builds = await listBuildsTool.transformResponse!( + responseJson({ + value: [ + { + id: 201, + buildNumber: '20260515.1', + status: 'completed', + result: 'failed', + queueTime: '2026-05-15T10:00:00Z', + sourceBranch: 'refs/heads/main', + sourceVersion: 'abc123', + }, + ], + }) + ) + + expect(builds.output.metadata.builds[0].definition).toEqual({ id: 0, name: '' }) + + const logs = await listBuildLogsTool.transformResponse!( + responseJson({ + count: 1, + value: [ + { + id: 3, + type: 'Container', + url: 'https://example/log/3', + lineCount: 25, + createdOn: '2026-05-15T10:00:00Z', + }, + ], + }) + ) + + expect(logs.output.metadata.logs[0].lineCount).toBe(25) + + const log = await getBuildLogTool.transformResponse!( + new Response('line one\nline two\nline three\n') + ) + + expect(log.output.metadata.lineCount).toBe(3) + await expect(getBuildLogTool.transformResponse!(new Response(' '))).resolves.toEqual({ + success: true, + output: { content: 'Log is empty.', metadata: { lineCount: 0 } }, + }) + }) + + it('transforms work item references and hydrated work items', async () => { + const betweenBuilds = await getWorkItemsBetweenBuildsTool.transformResponse!( + responseJson({ value: [{ id: 101, url: 'https://example/workitems/101' }] }) + ) + + expect(betweenBuilds.output.metadata.workItems).toEqual([ + { id: '101', url: 'https://example/workitems/101' }, + ]) + + const getWorkItem = await getWorkItemTool.transformResponse!(responseJson(rawWorkItem)) + expect(getWorkItem.output.metadata.workItem).toEqual({ + id: 101, + title: 'SimIntegrationTest Issue', + state: 'Doing', + workItemType: 'Issue', + assignedTo: 'Ada Lovelace', + areaPath: 'Fabrikam\\Platform', + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101', + }) + + const batch = await getWorkItemsBatchTool.transformResponse!( + responseJson({ value: [rawWorkItem] }) + ) + expect(batch.output.metadata.count).toBe(1) + }) + + it('hydrates WIQL query results with a second fetch and caps IDs at 200', async () => { + const fetchMock = vi.fn().mockResolvedValue(responseJson({ value: [rawWorkItem] })) + globalThis.fetch = fetchMock as unknown as typeof fetch + + const workItems = Array.from({ length: 201 }, (_, index) => ({ + id: index + 1, + url: `https://example/workitems/${index + 1}`, + })) + + const result = await queryWorkItemsTool.transformResponse!(responseJson({ workItems }), { + ...baseParams, + wiqlQuery: 'SELECT [System.Id] FROM workitems', + } satisfies QueryWorkItemsParams) + + const detailsUrl = new URL(String(fetchMock.mock.calls[0][0])) + expect(detailsUrl.searchParams.get('ids')?.split(',')).toHaveLength(200) + expect(result.output.metadata.workItems).toHaveLength(1) + }) + + it('does not hydrate WIQL empty results', async () => { + const fetchMock = vi.fn() + globalThis.fetch = fetchMock as unknown as typeof fetch + + const result = await queryWorkItemsTool.transformResponse!(responseJson({ workItems: [] }), { + ...baseParams, + wiqlQuery: 'SELECT [System.Id] FROM workitems WHERE [System.Id] = 0', + } satisfies QueryWorkItemsParams) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result.output.metadata).toEqual({ count: 0, workItems: [] }) + }) + + it('transforms create and update work item responses', async () => { + const created = await createWorkItemTool.transformResponse!(responseJson(rawWorkItem)) + expect(created.output.content).toContain('Created work item #101') + + const updated = await updateWorkItemTool.transformResponse!(responseJson(rawWorkItem)) + expect(updated.output.content).toContain('Updated work item #101') + }) + + it('transforms comment responses and empty comment lists', async () => { + const added = await addCommentTool.transformResponse!(responseJson(rawComment)) + expect(added.output.metadata.comment).toEqual({ + workItemId: 101, + commentId: 9, + version: 1, + text: 'SimIntegrationTest comment', + renderedText: '

SimIntegrationTest comment

', + createdBy: 'Ada Lovelace', + createdDate: '2026-05-15T10:00:00Z', + modifiedBy: 'Ada Lovelace', + modifiedDate: '2026-05-15T10:00:00Z', + isDeleted: false, + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments/9', + }) + + const comments = await getCommentsTool.transformResponse!( + responseJson({ + count: 1, + totalCount: 2, + comments: [rawComment], + continuationToken: 'next', + nextPage: 'https://example/next', + }) + ) + expect(comments.output.metadata.count).toBe(1) + expect(comments.output.metadata.continuationToken).toBe('next') + + const empty = await getCommentsTool.transformResponse!(responseJson({ comments: [] })) + expect(empty.output).toEqual({ + content: 'No comments found for this work item.', + metadata: { count: 0, totalCount: 0, comments: [] }, + }) + }) +}) From 6510f650732fe8107c1c7b585a82b57a359b4a87 Mon Sep 17 00:00:00 2001 From: mzxchandra <129460234+mzxchandra@users.noreply.github.com> Date: Sun, 17 May 2026 02:04:22 -0500 Subject: [PATCH 13/16] Update apps/sim/triggers/azure_devops/utils.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/triggers/azure_devops/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/triggers/azure_devops/utils.ts b/apps/sim/triggers/azure_devops/utils.ts index 9c44c6224e5..aeab35f8928 100644 --- a/apps/sim/triggers/azure_devops/utils.ts +++ b/apps/sim/triggers/azure_devops/utils.ts @@ -254,8 +254,10 @@ export function formatWorkItemCreatedInput(body: Record): Recor workItemType: (fields['System.WorkItemType'] as string) ?? '', title: (fields['System.Title'] as string) ?? '', state: (fields['System.State'] as string) ?? '', - createdBy: (fields['System.CreatedBy'] as string) ?? '', - assignedTo: (fields['System.AssignedTo'] as string) ?? '', + createdBy: + (fields['System.CreatedBy'] as { displayName?: string } | undefined)?.displayName ?? '', + assignedTo: + (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? '' priority: Number(fields['Microsoft.VSTS.Common.Priority'] ?? 0), areaPath: (fields['System.AreaPath'] as string) ?? '', iterationPath: (fields['System.IterationPath'] as string) ?? '', From 73c0a0b25c95227feeb69ee24e6cc3174db1baf7 Mon Sep 17 00:00:00 2001 From: mzxchandra <129460234+mzxchandra@users.noreply.github.com> Date: Sun, 17 May 2026 02:04:54 -0500 Subject: [PATCH 14/16] Update apps/sim/tools/azure_devops/update_work_item.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/tools/azure_devops/update_work_item.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/sim/tools/azure_devops/update_work_item.ts b/apps/sim/tools/azure_devops/update_work_item.ts index 2d236d73b4c..53e5807a34f 100644 --- a/apps/sim/tools/azure_devops/update_work_item.ts +++ b/apps/sim/tools/azure_devops/update_work_item.ts @@ -142,6 +142,12 @@ export const updateWorkItemTool: ToolConfig { const ops: AzureDevOpsJsonPatchOp[] = [] + if (!params.title && !params.description && !params.assignedTo && !params.areaPath && + params.priority === undefined && !params.state && params.effort === undefined && + !params.startDate && !params.targetDate && !params.activity && + params.remainingWork === undefined && params.completedWork === undefined && !params.tags) { + throw new Error('Update Work Item requires at least one field to update.') + } if (params.title) { ops.push({ op: 'replace', path: '/fields/System.Title', value: params.title }) } From 64fd6fc314ac3803946f1b7078de68433063fccd Mon Sep 17 00:00:00 2001 From: mzxchandra <129460234+mzxchandra@users.noreply.github.com> Date: Sun, 17 May 2026 02:05:23 -0500 Subject: [PATCH 15/16] Update apps/sim/triggers/azure_devops/utils.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/sim/triggers/azure_devops/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/triggers/azure_devops/utils.ts b/apps/sim/triggers/azure_devops/utils.ts index aeab35f8928..618abd1c91a 100644 --- a/apps/sim/triggers/azure_devops/utils.ts +++ b/apps/sim/triggers/azure_devops/utils.ts @@ -60,7 +60,7 @@ export function isAzureDevOpsEventMatch( } const resource = body.resource as Record | undefined const result = resource?.result as string | undefined - return result !== 'succeeded' + return result === 'failed' || result === 'canceled' || result === 'partiallySucceeded' } if (triggerId === 'azure_devops_work_item_created') { From ddeca14bde2ab5ecef6e0fe573e690255ee52b5d Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sun, 17 May 2026 04:36:19 -0500 Subject: [PATCH 16/16] comma syntax error patched --- apps/sim/triggers/azure_devops/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/triggers/azure_devops/utils.ts b/apps/sim/triggers/azure_devops/utils.ts index 618abd1c91a..e5f59c106a5 100644 --- a/apps/sim/triggers/azure_devops/utils.ts +++ b/apps/sim/triggers/azure_devops/utils.ts @@ -257,7 +257,7 @@ export function formatWorkItemCreatedInput(body: Record): Recor createdBy: (fields['System.CreatedBy'] as { displayName?: string } | undefined)?.displayName ?? '', assignedTo: - (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? '' + (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? '', priority: Number(fields['Microsoft.VSTS.Common.Priority'] ?? 0), areaPath: (fields['System.AreaPath'] as string) ?? '', iterationPath: (fields['System.IterationPath'] as string) ?? '',