From e3c9ada2a188a425dd7daffa72f30af0c0ee5bd0 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:56:22 -0500 Subject: [PATCH 01/24] 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 6508cb8bbb..2fd22eb01a 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3126,6 +3126,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 27fcd253558f3ae8350a3a147b7b36667505bbb6 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:58:35 -0500 Subject: [PATCH 02/24] 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 0000000000..29bac84e6a --- /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 5fd456823b50279329db5299a4bc3ce87be518cb Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 22:58:59 -0500 Subject: [PATCH 03/24] 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 5448fe6407..5e8a637079 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 22cd7ae2ebf20cc7b3bb2403ce274313c4acbac6 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:02:50 -0500 Subject: [PATCH 04/24] added ADO to registries --- .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 103 ++++++++++++++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/tools/registry.ts | 34 ++++++ 4 files changed, 141 insertions(+) diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index ab6f6b1831..9a1c6030bc 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, @@ -226,6 +227,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 b3d8b3bc4f..97d158f5e5 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", @@ -4057,6 +4156,10 @@ "name": "Fetch", "description": "Fetch and parse a file from a URL with optional custom headers." }, + { + "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., " diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index ce8e9c6af7..5c4d8eaea7 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 b65d6bda69..380fed8915 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, @@ -3215,6 +3233,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 0db4cff127e51fa2af01f251aaaed3ab4a8ef6ef Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:04:43 -0500 Subject: [PATCH 05/24] 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 0000000000..acd01f5175 --- /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 0000000000..5ef419ea36 --- /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 0000000000..9c44c6224e --- /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 0000000000..fda0442409 --- /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 0000000000..752485add7 --- /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 1bacff0b3c45a55e7ae73e6241e5966e19a42a3e Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:05:09 -0500 Subject: [PATCH 06/24] 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 b1d747f529..e458c36b64 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 39eaa61229d376e46d94b7cfdec31dfc7322c9a1 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:06:35 -0500 Subject: [PATCH 07/24] 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 0000000000..bc731aca7a --- /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 0000000000..73c721ad6a --- /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 0000000000..96bea77e67 --- /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 0000000000..f5f3844180 --- /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 0000000000..0c5328e59e --- /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 0000000000..4bad50ea5e --- /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 0000000000..89db7f7804 --- /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 0000000000..667266474d --- /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 0000000000..0ef221b730 --- /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 0000000000..09400c9abb --- /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 0000000000..0d381d9c47 --- /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 0000000000..d57a24eec5 --- /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 0000000000..34fe1b5ef7 --- /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 0000000000..c4dbbc54db --- /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 0000000000..11de874545 --- /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 0000000000..de6aef1147 --- /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 0000000000..17f2bee998 --- /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 0000000000..2d236d73b4 --- /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 0000000000..b095549855 --- /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 15c617eb08312cc0054ffeb32a05772558a3c61d Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:07:09 -0500 Subject: [PATCH 08/24] 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 0000000000..f72c0fdb20 --- /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 8e07ef1ca38ea55bbcb380f220a65a1b4897a183 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:08:23 -0500 Subject: [PATCH 09/24] 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 0000000000..b511333210 --- /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 2e008d00ed..64c0e44859 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' @@ -258,6 +259,7 @@ export const registry: Record = { ashby: AshbyBlock, athena: AthenaBlock, attio: AttioBlock, + azure_devops: AzureDevOpsBlock, box: BoxBlock, brandfetch: BrandfetchBlock, brightdata: BrightDataBlock, From 042410ef5ee54dc06dea23199bdb8693b15d9fa6 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sat, 16 May 2026 23:08:58 -0500 Subject: [PATCH 10/24] 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 6508cb8bbb..ab23472596 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3126,6 +3126,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/24] 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 0000000000..c329d55b2e --- /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 70d13afd92..ab14483dd5 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 c2a150d51b325c21e40d63e91bd3e587362796e5 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sun, 17 May 2026 01:43:39 -0500 Subject: [PATCH 12/24] 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 0000000000..8180f6d60e --- /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 0000000000..dd6ec4dfbc --- /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 e49a52cb07494c607e93bf2204f57a3268a06499 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/24] 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 9c44c6224e..aeab35f892 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 1bdfd79074d42786119d4276fd639e2776f7107a 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/24] 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 2d236d73b4..53e5807a34 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 61b4d91a183b38c93d8cf6e1c729aa0609f31ae5 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/24] 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 aeab35f892..618abd1c91 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 d11aa83c7d81e3a1d89b4c6f1912636745564420 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Sun, 17 May 2026 04:36:19 -0500 Subject: [PATCH 16/24] 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 618abd1c91..e5f59c106a 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) ?? '', From c084b6c9e2833d4e905f9fb7b9b67a114f2bfe65 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 12:27:25 -0700 Subject: [PATCH 17/24] azure devops: validate-integration fixes + manual description - bgColor switched from white to Azure DevOps brand color #0078D4 (block + mdx) - WIQL query_work_items: hydrate ALL matched IDs by chunking through batches of 200 instead of silently truncating; check response.ok on the follow-up fetch and surface a clear error on 4xx/5xx; trim org/project; expose totalMatched in metadata so users can see pre-hydration count - Add MANUAL-CONTENT-START:intro section to the azure_devops.mdx docs page - Update unit tests for new chunking behavior and update-work-item validation Co-Authored-By: Claude Opus 4.7 --- .../content/docs/en/tools/azure_devops.mdx | 19 +++++- apps/sim/blocks/blocks/azure_devops.ts | 8 ++- .../tools/azure_devops/azure-devops.test.ts | 34 ++++++++-- .../tools/azure_devops/query_work_items.ts | 67 ++++++++++++------- apps/sim/tools/azure_devops/types.ts | 1 + 5 files changed, 95 insertions(+), 34 deletions(-) diff --git a/apps/docs/content/docs/en/tools/azure_devops.mdx b/apps/docs/content/docs/en/tools/azure_devops.mdx index 29bac84e6a..38b3bfb69d 100644 --- a/apps/docs/content/docs/en/tools/azure_devops.mdx +++ b/apps/docs/content/docs/en/tools/azure_devops.mdx @@ -7,9 +7,26 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" +{/* MANUAL-CONTENT-START:intro */} +[Azure DevOps](https://azure.microsoft.com/en-us/products/devops) is Microsoft's end-to-end DevOps platform for planning, building, testing, and shipping software. It powers engineering at tens of thousands of enterprises across automotive, financial services, government, and any organization built on the Microsoft stack. + +With the Azure DevOps integration in Sim, you can: + +- **Inspect pipelines and runs**: List pipelines, fetch metadata, and walk through run history with status and result +- **Triage build failures**: Pull build timelines to see which stage, job, or task failed, then fetch the exact log for the failing step +- **Audit changes between builds**: Surface the work items that landed between any two builds — useful for release notes and regression hunts +- **Query work items with WIQL**: Run full WIQL queries and get hydrated work item fields back in a single call, not just IDs +- **Manage work item lifecycle**: Create, update, and read Issues, Tasks, and Epics with structured fields — title, description, priority, assignee, area path, iteration, tags, effort, and dates +- **Collaborate via comments**: Add internal or public comments to work items and read full comment history +- **React in real time**: Trigger workflows when builds fail or new work items are created via Azure DevOps service hooks + +These capabilities let your Sim agents close the loop on the DevOps lifecycle — automatically triaging broken builds, drafting release notes between deployments, syncing work items across systems, and keeping engineering operations running while your team focuses on shipping. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments. diff --git a/apps/sim/blocks/blocks/azure_devops.ts b/apps/sim/blocks/blocks/azure_devops.ts index b511333210..970fe914bf 100644 --- a/apps/sim/blocks/blocks/azure_devops.ts +++ b/apps/sim/blocks/blocks/azure_devops.ts @@ -22,7 +22,7 @@ export const AzureDevOpsBlock: BlockConfig = { category: 'tools', integrationType: IntegrationType.DeveloperTools, tags: ['ci-cd', 'project-management', 'version-control'], - bgColor: '#FFFFFF', + bgColor: '#0078D4', icon: AzureDevOpsIcon, authMode: AuthMode.ApiKey, triggerAllowed: true, @@ -136,7 +136,11 @@ export const AzureDevOpsBlock: BlockConfig = { required: true, condition: { field: 'operation', - value: ['azure_devops_list_build_logs', 'azure_devops_get_build_log', 'azure_devops_get_build_timeline'], + value: [ + 'azure_devops_list_build_logs', + 'azure_devops_get_build_log', + 'azure_devops_get_build_timeline', + ], }, }, { diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index dd6ec4dfbc..bfaa8aa15d 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -317,7 +317,9 @@ describe('Azure DevOps request builders', () => { { 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([]) + expect(() => buildBody(updateWorkItemTool, { ...baseParams, workItemId: 101 })).toThrow( + /requires at least one field/ + ) const createWithEffortParams = { ...createParams, @@ -575,8 +577,10 @@ describe('Azure DevOps response transforms', () => { 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] })) + it('hydrates WIQL query results in chunks of 200 IDs', async () => { + const fetchMock = vi + .fn() + .mockImplementation(() => Promise.resolve(responseJson({ value: [rawWorkItem] }))) globalThis.fetch = fetchMock as unknown as typeof fetch const workItems = Array.from({ length: 201 }, (_, index) => ({ @@ -589,9 +593,27 @@ describe('Azure DevOps response transforms', () => { 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) + expect(fetchMock).toHaveBeenCalledTimes(2) + const firstChunk = new URL(String(fetchMock.mock.calls[0][0])) + const secondChunk = new URL(String(fetchMock.mock.calls[1][0])) + expect(firstChunk.searchParams.get('ids')?.split(',')).toHaveLength(200) + expect(secondChunk.searchParams.get('ids')?.split(',')).toHaveLength(1) + expect(result.output.metadata.totalMatched).toBe(201) + expect(result.output.metadata.workItems).toHaveLength(2) + }) + + it('throws when WIQL hydration fetch returns a non-OK status', async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(new Response('forbidden', { status: 403, statusText: 'Forbidden' })) + globalThis.fetch = fetchMock as unknown as typeof fetch + + await expect( + queryWorkItemsTool.transformResponse!(responseJson({ workItems: [{ id: 1, url: 'x' }] }), { + ...baseParams, + wiqlQuery: 'SELECT [System.Id] FROM workitems', + } satisfies QueryWorkItemsParams) + ).rejects.toThrow(/Failed to hydrate work item details/) }) it('does not hydrate WIQL empty results', async () => { diff --git a/apps/sim/tools/azure_devops/query_work_items.ts b/apps/sim/tools/azure_devops/query_work_items.ts index de6aef1147..f57c6c901d 100644 --- a/apps/sim/tools/azure_devops/query_work_items.ts +++ b/apps/sim/tools/azure_devops/query_work_items.ts @@ -44,7 +44,7 @@ export const queryWorkItemsTool: ToolConfig - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/wiql?api-version=7.2-preview.2`, + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/wiql?api-version=7.2-preview.2`, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', @@ -67,41 +67,53 @@ export const queryWorkItemsTool: ToolConfig wi.id) - .join(',') + const allIds = workItemRefs.map((wi) => wi.id) + const BATCH_SIZE = 200 + const organization = params!.organization.trim() + const project = params!.project.trim() + const authHeader = `Basic ${btoa(`:${params!.accessToken}`)}` - 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 workItems: AzureDevOpsWorkItem[] = [] + for (let i = 0; i < allIds.length; i += BATCH_SIZE) { + const chunk = allIds.slice(i, i + BATCH_SIZE) + const detailsUrl = new URL( + `https://dev.azure.com/${organization}/${project}/_apis/wit/workitems` + ) + detailsUrl.searchParams.set('ids', chunk.join(',')) + 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 detailsResponse = await fetch(detailsUrl.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: authHeader, + }, + }) - const detailsData = await detailsResponse.json() - const workItems: AzureDevOpsWorkItem[] = (detailsData.value ?? []).map( - (raw: AzureDevOpsRawWorkItem) => mapWorkItem(raw) - ) + if (!detailsResponse.ok) { + const errorBody = await detailsResponse.text().catch(() => '') + throw new Error( + `Failed to hydrate work item details (${detailsResponse.status}): ${errorBody || detailsResponse.statusText}` + ) + } + + const detailsData = await detailsResponse.json() + for (const raw of detailsData.value ?? []) { + workItems.push(mapWorkItem(raw as AzureDevOpsRawWorkItem)) + } + } const content = workItems.length === 0 ? 'No work item details found.' - : `Found ${workItems.length} work item(s):\n\n${workItems.map(formatWorkItem).join('\n\n')}` + : `Found ${workItems.length} work item(s) (of ${allIds.length} matched):\n\n${workItems.map(formatWorkItem).join('\n\n')}` return { success: true, output: { content, - metadata: { count: workItems.length, workItems }, + metadata: { count: workItems.length, totalMatched: allIds.length, workItems }, }, } }, @@ -115,7 +127,12 @@ export const queryWorkItemsTool: ToolConfig Date: Tue, 19 May 2026 12:40:48 -0700 Subject: [PATCH 18/24] azure_devops: second-pass audit fixes + formatter cleanup - Add types barrel export to tools/azure_devops/index.ts - Normalize comment endpoint path casing (/workItems/ -> /workitems/) - Update test assertions to match normalized path - Biome formatter reflow across tools, triggers, registry, and docs icon Co-Authored-By: Claude Opus 4.7 --- apps/docs/components/icons.tsx | 56 +++++++++---------- apps/sim/tools/azure_devops/add_comment.ts | 2 +- .../tools/azure_devops/azure-devops.test.ts | 8 +-- .../tools/azure_devops/create_work_item.ts | 19 +++++-- apps/sim/tools/azure_devops/get_comments.ts | 2 +- apps/sim/tools/azure_devops/index.ts | 2 + .../tools/azure_devops/update_work_item.ts | 33 ++++++++--- apps/sim/tools/registry.ts | 6 +- .../sim/triggers/azure_devops/build_failed.ts | 2 +- apps/sim/triggers/azure_devops/utils.ts | 9 +-- .../azure_devops/work_item_created.ts | 2 +- apps/sim/triggers/registry.ts | 10 ++-- 12 files changed, 87 insertions(+), 64 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 2fd22eb01a..ab23472596 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3127,34 +3127,34 @@ export function AzureIcon(props: SVGProps) { } export function AzureDevOpsIcon(props: SVGProps) { - const id = useId() - const gradientId = `azure_devops_gradient_${id}` - return ( - - - - - - - - - - - - - ) - } + const id = useId() + const gradientId = `azure_devops_gradient_${id}` + return ( + + + + + + + + + + + + + ) +} export const GroqIcon = (props: SVGProps) => ( = request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workItems/${Number(params.workItemId)}/comments` + `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() diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index bfaa8aa15d..f773fe4ce6 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -102,7 +102,7 @@ const rawComment = { 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', + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101/comments/9', id: 9, } @@ -418,7 +418,7 @@ describe('Azure DevOps request builders', () => { } satisfies AddCommentParams expect(buildUrl(addCommentTool, addParams)).toBe( - 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments?api-version=7.2-preview.4' + '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', @@ -435,7 +435,7 @@ describe('Azure DevOps request builders', () => { 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' + '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' ) }) }) @@ -650,7 +650,7 @@ describe('Azure DevOps response transforms', () => { modifiedBy: 'Ada Lovelace', modifiedDate: '2026-05-15T10:00:00Z', isDeleted: false, - url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workItems/101/comments/9', + url: 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101/comments/9', }) const comments = await getCommentsTool.transformResponse!( diff --git a/apps/sim/tools/azure_devops/create_work_item.ts b/apps/sim/tools/azure_devops/create_work_item.ts index 73c721ad6a..01af061eda 100644 --- a/apps/sim/tools/azure_devops/create_work_item.ts +++ b/apps/sim/tools/azure_devops/create_work_item.ts @@ -67,8 +67,7 @@ export const createWorkItemTool: ToolConfig request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workItems/${Number(params.workItemId)}/comments` + `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()) diff --git a/apps/sim/tools/azure_devops/index.ts b/apps/sim/tools/azure_devops/index.ts index 0d381d9c47..d386684840 100644 --- a/apps/sim/tools/azure_devops/index.ts +++ b/apps/sim/tools/azure_devops/index.ts @@ -15,6 +15,8 @@ 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 * from '@/tools/azure_devops/types' + export { listPipelinesTool, getPipelineTool, diff --git a/apps/sim/tools/azure_devops/update_work_item.ts b/apps/sim/tools/azure_devops/update_work_item.ts index 53e5807a34..4ec19f0c05 100644 --- a/apps/sim/tools/azure_devops/update_work_item.ts +++ b/apps/sim/tools/azure_devops/update_work_item.ts @@ -73,15 +73,13 @@ 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) { + 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) { @@ -185,7 +194,13 @@ export const updateWorkItemTool: ToolConfig `
${i + 1}. ${s}
`) - .join('') + return steps.map((s, i) => `
${i + 1}. ${s}
`).join('') } export const buildFailedSetupInstructions = instructions([ @@ -44,10 +42,7 @@ export const webhookSetupInstructions = instructions([ /** * Returns whether an Azure DevOps service hook payload matches the configured trigger. */ -export function isAzureDevOpsEventMatch( - triggerId: string, - body: Record -): boolean { +export function isAzureDevOpsEventMatch(triggerId: string, body: Record): boolean { if (triggerId === 'azure_devops_webhook') { return true } diff --git a/apps/sim/triggers/azure_devops/work_item_created.ts b/apps/sim/triggers/azure_devops/work_item_created.ts index 752485add7..289ddf4676 100644 --- a/apps/sim/triggers/azure_devops/work_item_created.ts +++ b/apps/sim/triggers/azure_devops/work_item_created.ts @@ -1,11 +1,11 @@ import { AzureDevOpsIcon } from '@/components/icons' import { buildTriggerSubBlocks } from '@/triggers' -import type { TriggerConfig } from '@/triggers/types' import { azureDevOpsTriggerOptions, buildWorkItemCreatedOutputs, workItemCreatedSetupInstructions, } from '@/triggers/azure_devops/utils' +import type { TriggerConfig } from '@/triggers/types' export const azureDevOpsWorkItemCreatedTrigger: TriggerConfig = { id: 'azure_devops_work_item_created', diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index e458c36b64..bb4d252d75 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -7,11 +7,6 @@ import { ashbyJobCreateTrigger, ashbyOfferCreateTrigger, } from '@/triggers/ashby' -import { - azureDevOpsBuildFailedTrigger, - azureDevOpsWebhookTrigger, - azureDevOpsWorkItemCreatedTrigger, -} from '@/triggers/azure_devops' import { attioCommentCreatedTrigger, attioCommentDeletedTrigger, @@ -36,6 +31,11 @@ import { attioWebhookTrigger, attioWorkspaceMemberCreatedTrigger, } from '@/triggers/attio' +import { + azureDevOpsBuildFailedTrigger, + azureDevOpsWebhookTrigger, + azureDevOpsWorkItemCreatedTrigger, +} from '@/triggers/azure_devops' import { calcomBookingCancelledTrigger, calcomBookingCreatedTrigger, From 119ac04e2e0c1a5817014794e77d9ec12a9fd020 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 12:43:54 -0700 Subject: [PATCH 19/24] azure_devops: address PR review comments - Fix bgColor #FFFFFF -> #0078D4 in integrations.json and triggers/azure_devops.mdx - Bump File tool operationCount from 4 to 5 (Read, Fetch, Get, Write, Append) - Apply .trim() to org/project across all 15 remaining tools (consistency with query_work_items) - Fix Found ${data.count} -> Found ${data.count ?? items.length} fallback in list_builds, list_pipelines, list_pipeline_runs content strings Co-Authored-By: Claude Opus 4.7 --- .../content/docs/en/triggers/azure_devops.mdx | 2 +- .../integrations/data/integrations.json | 4 +-- .../lib/webhooks/providers/azure-devops.ts | 28 +++++++++++++++++-- apps/sim/tools/azure_devops/add_comment.ts | 2 +- .../tools/azure_devops/create_work_item.ts | 2 +- apps/sim/tools/azure_devops/get_build_log.ts | 2 +- .../tools/azure_devops/get_build_timeline.ts | 2 +- apps/sim/tools/azure_devops/get_comments.ts | 2 +- apps/sim/tools/azure_devops/get_pipeline.ts | 2 +- .../tools/azure_devops/get_pipeline_run.ts | 2 +- apps/sim/tools/azure_devops/get_work_item.ts | 2 +- .../azure_devops/get_work_items_batch.ts | 2 +- .../get_work_items_between_builds.ts | 2 +- .../sim/tools/azure_devops/list_build_logs.ts | 2 +- apps/sim/tools/azure_devops/list_builds.ts | 4 +-- .../tools/azure_devops/list_pipeline_runs.ts | 4 +-- apps/sim/tools/azure_devops/list_pipelines.ts | 4 +-- .../tools/azure_devops/update_work_item.ts | 2 +- apps/sim/triggers/azure_devops/utils.ts | 18 ++++++------ 19 files changed, 56 insertions(+), 32 deletions(-) diff --git a/apps/docs/content/docs/en/triggers/azure_devops.mdx b/apps/docs/content/docs/en/triggers/azure_devops.mdx index c329d55b2e..d22c9ae825 100644 --- a/apps/docs/content/docs/en/triggers/azure_devops.mdx +++ b/apps/docs/content/docs/en/triggers/azure_devops.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" Azure Devops provides 3 triggers for automating workflows based on events. diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 97d158f5e5..cca205b900 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -1888,7 +1888,7 @@ "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", + "bgColor": "#0078D4", "iconName": "AzureDevOpsIcon", "docsUrl": "https://docs.sim.ai/tools/azure_devops", "operations": [ @@ -4169,7 +4169,7 @@ "description": "Append content to an existing workspace file. The file must already exist. Content is added to the end of the file." } ], - "operationCount": 4, + "operationCount": 5, "triggers": [], "triggerCount": 0, "authType": "none", diff --git a/apps/sim/lib/webhooks/providers/azure-devops.ts b/apps/sim/lib/webhooks/providers/azure-devops.ts index f72c0fdb20..4c89b62660 100644 --- a/apps/sim/lib/webhooks/providers/azure-devops.ts +++ b/apps/sim/lib/webhooks/providers/azure-devops.ts @@ -45,7 +45,31 @@ export const azureDevOpsHandler: WebhookProviderHandler = { return true }, - async formatInput({ body, webhook }: FormatInputContext): Promise { + extractIdempotencyId(body: unknown): string | null { + const obj = body as Record | null + if (!obj) return null + const notificationId = obj.notificationId + const subscriptionId = obj.subscriptionId + if ( + (typeof notificationId === 'number' || typeof notificationId === 'string') && + typeof subscriptionId === 'string' && + subscriptionId + ) { + return `azure_devops:${subscriptionId}:${notificationId}` + } + const eventType = obj.eventType + const resource = obj.resource as Record | undefined + const resourceId = resource?.id + if ( + typeof eventType === 'string' && + (typeof resourceId === 'number' || typeof resourceId === 'string') + ) { + return `azure_devops:${eventType}:${resourceId}` + } + return null + }, + + async formatInput({ body, webhook, requestId }: FormatInputContext): Promise { const b = body as Record const providerConfig = (webhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined @@ -63,7 +87,7 @@ export const azureDevOpsHandler: WebhookProviderHandler = { return { input: formatWorkItemCreatedInput(b) } } - logger.warn('Azure DevOps: unknown eventType for specialized trigger', { + logger.warn(`[${requestId}] Azure DevOps: unknown eventType for specialized trigger`, { triggerId, eventType, }) diff --git a/apps/sim/tools/azure_devops/add_comment.ts b/apps/sim/tools/azure_devops/add_comment.ts index f82efc7a67..5d307f3daf 100644 --- a/apps/sim/tools/azure_devops/add_comment.ts +++ b/apps/sim/tools/azure_devops/add_comment.ts @@ -49,7 +49,7 @@ export const addCommentTool: ToolConfig = request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}/comments` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems/${Number(params.workItemId)}/comments` ) url.searchParams.set('api-version', '7.2-preview.4') return url.toString() diff --git a/apps/sim/tools/azure_devops/create_work_item.ts b/apps/sim/tools/azure_devops/create_work_item.ts index 01af061eda..8ba561930c 100644 --- a/apps/sim/tools/azure_devops/create_work_item.ts +++ b/apps/sim/tools/azure_devops/create_work_item.ts @@ -132,7 +132,7 @@ export const createWorkItemTool: ToolConfig - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/$${encodeURIComponent(params.workItemType)}?api-version=7.2-preview.3`, + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems/$${encodeURIComponent(params.workItemType)}?api-version=7.2-preview.3`, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json-patch+json', diff --git a/apps/sim/tools/azure_devops/get_build_log.ts b/apps/sim/tools/azure_devops/get_build_log.ts index 96bea77e67..300082db80 100644 --- a/apps/sim/tools/azure_devops/get_build_log.ts +++ b/apps/sim/tools/azure_devops/get_build_log.ts @@ -56,7 +56,7 @@ export const getBuildLogTool: ToolConfig request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${params.buildId}/logs/${params.logId}` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/build/builds/${params.buildId}/logs/${params.logId}` ) url.searchParams.set('api-version', '7.2-preview.2') if (params.startLine !== undefined) diff --git a/apps/sim/tools/azure_devops/get_build_timeline.ts b/apps/sim/tools/azure_devops/get_build_timeline.ts index f5f3844180..ec14df7ac1 100644 --- a/apps/sim/tools/azure_devops/get_build_timeline.ts +++ b/apps/sim/tools/azure_devops/get_build_timeline.ts @@ -41,7 +41,7 @@ export const getBuildTimelineTool: ToolConfig - `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${Number(params.buildId)}/timeline?api-version=7.2-preview.3`, + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/build/builds/${Number(params.buildId)}/timeline?api-version=7.2-preview.3`, method: 'GET', headers: (params) => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/azure_devops/get_comments.ts b/apps/sim/tools/azure_devops/get_comments.ts index 1f8f28df3c..0b41036ec4 100644 --- a/apps/sim/tools/azure_devops/get_comments.ts +++ b/apps/sim/tools/azure_devops/get_comments.ts @@ -74,7 +74,7 @@ export const getCommentsTool: ToolConfig request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}/comments` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_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()) diff --git a/apps/sim/tools/azure_devops/get_pipeline.ts b/apps/sim/tools/azure_devops/get_pipeline.ts index 4bad50ea5e..c08af037ec 100644 --- a/apps/sim/tools/azure_devops/get_pipeline.ts +++ b/apps/sim/tools/azure_devops/get_pipeline.ts @@ -44,7 +44,7 @@ export const getPipelineTool: ToolConfig request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/pipelines/${params.pipelineId}` ) url.searchParams.set('api-version', '7.2-preview.1') if (params.pipelineVersion) diff --git a/apps/sim/tools/azure_devops/get_pipeline_run.ts b/apps/sim/tools/azure_devops/get_pipeline_run.ts index 89db7f7804..e17959e345 100644 --- a/apps/sim/tools/azure_devops/get_pipeline_run.ts +++ b/apps/sim/tools/azure_devops/get_pipeline_run.ts @@ -44,7 +44,7 @@ export const getPipelineRunTool: ToolConfig { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}/runs/${params.runId}` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/pipelines/${params.pipelineId}/runs/${params.runId}` ) url.searchParams.set('api-version', '7.2-preview.1') return url.toString() diff --git a/apps/sim/tools/azure_devops/get_work_item.ts b/apps/sim/tools/azure_devops/get_work_item.ts index 667266474d..f1731cd444 100644 --- a/apps/sim/tools/azure_devops/get_work_item.ts +++ b/apps/sim/tools/azure_devops/get_work_item.ts @@ -40,7 +40,7 @@ export const getWorkItemTool: ToolConfig request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems/${Number(params.workItemId)}` ) url.searchParams.set('$expand', 'all') url.searchParams.set('api-version', '7.2-preview.3') diff --git a/apps/sim/tools/azure_devops/get_work_items_batch.ts b/apps/sim/tools/azure_devops/get_work_items_batch.ts index 0ef221b730..86de8de333 100644 --- a/apps/sim/tools/azure_devops/get_work_items_batch.ts +++ b/apps/sim/tools/azure_devops/get_work_items_batch.ts @@ -46,7 +46,7 @@ export const getWorkItemsBatchTool: ToolConfig { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems` ) url.searchParams.set('ids', params.ids) url.searchParams.set('$expand', 'all') 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 index 09400c9abb..17df94db64 100644 --- a/apps/sim/tools/azure_devops/get_work_items_between_builds.ts +++ b/apps/sim/tools/azure_devops/get_work_items_between_builds.ts @@ -51,7 +51,7 @@ export const getWorkItemsBetweenBuildsTool: ToolConfig< request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/workitems` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/build/workitems` ) url.searchParams.set('fromBuildId', Number(params.fromBuildId).toString()) url.searchParams.set('toBuildId', Number(params.toBuildId).toString()) diff --git a/apps/sim/tools/azure_devops/list_build_logs.ts b/apps/sim/tools/azure_devops/list_build_logs.ts index d57a24eec5..1f8183d02c 100644 --- a/apps/sim/tools/azure_devops/list_build_logs.ts +++ b/apps/sim/tools/azure_devops/list_build_logs.ts @@ -37,7 +37,7 @@ export const listBuildLogsTool: ToolConfig - `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds/${params.buildId}/logs?api-version=7.2-preview.2`, + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/build/builds/${params.buildId}/logs?api-version=7.2-preview.2`, method: 'GET', headers: (params) => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/azure_devops/list_builds.ts b/apps/sim/tools/azure_devops/list_builds.ts index 34fe1b5ef7..dfdc087ed2 100644 --- a/apps/sim/tools/azure_devops/list_builds.ts +++ b/apps/sim/tools/azure_devops/list_builds.ts @@ -63,7 +63,7 @@ export const listBuildsTool: ToolConfig = request: { url: (params) => { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/build/builds` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/build/builds` ) url.searchParams.set('api-version', '7.2-preview.8') if (params.definitionIds) url.searchParams.set('definitions', params.definitionIds) @@ -100,7 +100,7 @@ export const listBuildsTool: ToolConfig = const content = builds.length === 0 ? 'No builds found.' - : `Found ${data.count} build(s):\n\n${builds + : `Found ${data.count ?? builds.length} build(s):\n\n${builds .map( (b) => `- Build ${b.buildNumber} (ID: ${b.id})\n` + diff --git a/apps/sim/tools/azure_devops/list_pipeline_runs.ts b/apps/sim/tools/azure_devops/list_pipeline_runs.ts index c4dbbc54db..ce84c5eb82 100644 --- a/apps/sim/tools/azure_devops/list_pipeline_runs.ts +++ b/apps/sim/tools/azure_devops/list_pipeline_runs.ts @@ -38,7 +38,7 @@ export const listPipelineRunsTool: ToolConfig { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines/${params.pipelineId}/runs` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/pipelines/${params.pipelineId}/runs` ) url.searchParams.set('api-version', '7.2-preview.1') return url.toString() @@ -67,7 +67,7 @@ export const listPipelineRunsTool: ToolConfig `- Run ${r.name} (ID: ${r.id})\n` + diff --git a/apps/sim/tools/azure_devops/list_pipelines.ts b/apps/sim/tools/azure_devops/list_pipelines.ts index 11de874545..913aebfda9 100644 --- a/apps/sim/tools/azure_devops/list_pipelines.ts +++ b/apps/sim/tools/azure_devops/list_pipelines.ts @@ -50,7 +50,7 @@ export const listPipelinesTool: ToolConfig { const url = new URL( - `https://dev.azure.com/${params.organization}/${params.project}/_apis/pipelines` + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/pipelines` ) url.searchParams.set('api-version', '7.2-preview.1') if (params.orderBy) url.searchParams.set('orderBy', params.orderBy) @@ -82,7 +82,7 @@ export const listPipelinesTool: ToolConfig `- ${p.name} (ID: ${p.id})\n Folder: ${p.folder}\n URL: ${p.url}`) .join('\n')}` diff --git a/apps/sim/tools/azure_devops/update_work_item.ts b/apps/sim/tools/azure_devops/update_work_item.ts index 4ec19f0c05..e5e83c70a2 100644 --- a/apps/sim/tools/azure_devops/update_work_item.ts +++ b/apps/sim/tools/azure_devops/update_work_item.ts @@ -132,7 +132,7 @@ export const updateWorkItemTool: ToolConfig - `https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/workitems/${Number(params.workItemId)}?api-version=7.2-preview.3`, + `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems/${Number(params.workItemId)}?api-version=7.2-preview.3`, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json-patch+json', diff --git a/apps/sim/triggers/azure_devops/utils.ts b/apps/sim/triggers/azure_devops/utils.ts index 33eec7f235..fba1f689a0 100644 --- a/apps/sim/triggers/azure_devops/utils.ts +++ b/apps/sim/triggers/azure_devops/utils.ts @@ -105,7 +105,7 @@ export function buildBuildFailedOutputs(): Record { }, triggeredByEmail: { type: 'string', - description: 'Email/unique name of the person who triggered the build', + description: 'Email/unique name of the person who triggered the build, or null if not set', }, startTime: { type: 'string', @@ -142,11 +142,11 @@ export function buildWorkItemCreatedOutputs(): Record { }, createdBy: { type: 'string', - description: 'Display name of the creator', + description: 'Display name of the creator, or null if not set', }, assignedTo: { type: 'string', - description: 'Assignee display name, or empty string if unassigned', + description: 'Assignee display name, or null if unassigned', }, priority: { type: 'number', @@ -162,7 +162,7 @@ export function buildWorkItemCreatedOutputs(): Record { }, description: { type: 'string', - description: 'Work item description (HTML), or empty string if not set', + description: 'Work item description (HTML), or null if not set', }, projectName: { type: 'string', @@ -232,8 +232,8 @@ export function formatBuildCompleteInput(body: Record): Record< 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) ?? '', + triggeredBy: (requestedFor.displayName as string) ?? null, + triggeredByEmail: (requestedFor.uniqueName as string) ?? null, startTime: (resource.startTime as string) ?? '', finishTime: (resource.finishTime as string) ?? '', buildUrl: (resource.url as string) ?? '', @@ -250,13 +250,13 @@ export function formatWorkItemCreatedInput(body: Record): Recor title: (fields['System.Title'] as string) ?? '', state: (fields['System.State'] as string) ?? '', createdBy: - (fields['System.CreatedBy'] as { displayName?: string } | undefined)?.displayName ?? '', + (fields['System.CreatedBy'] as { displayName?: string } | undefined)?.displayName ?? null, assignedTo: - (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? '', + (fields['System.AssignedTo'] as { displayName?: string } | undefined)?.displayName ?? null, 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) ?? '', + description: (fields['System.Description'] as string) ?? null, projectName: (fields['System.TeamProject'] as string) ?? '', workItemUrl: (resource.url as string) ?? '', } From 609a647eb9e60d29eefd70c69a7b0e1f0b14724e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 13:04:33 -0700 Subject: [PATCH 20/24] idemtpotency --- .../lib/webhooks/providers/azure-devops.ts | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/azure-devops.ts b/apps/sim/lib/webhooks/providers/azure-devops.ts index 4c89b62660..0ec670cb26 100644 --- a/apps/sim/lib/webhooks/providers/azure-devops.ts +++ b/apps/sim/lib/webhooks/providers/azure-devops.ts @@ -48,25 +48,7 @@ export const azureDevOpsHandler: WebhookProviderHandler = { extractIdempotencyId(body: unknown): string | null { const obj = body as Record | null if (!obj) return null - const notificationId = obj.notificationId - const subscriptionId = obj.subscriptionId - if ( - (typeof notificationId === 'number' || typeof notificationId === 'string') && - typeof subscriptionId === 'string' && - subscriptionId - ) { - return `azure_devops:${subscriptionId}:${notificationId}` - } - const eventType = obj.eventType - const resource = obj.resource as Record | undefined - const resourceId = resource?.id - if ( - typeof eventType === 'string' && - (typeof resourceId === 'number' || typeof resourceId === 'string') - ) { - return `azure_devops:${eventType}:${resourceId}` - } - return null + return `azure_devops:${obj.subscriptionId}:${obj.notificationId}` }, async formatInput({ body, webhook, requestId }: FormatInputContext): Promise { From db0b46b53f2d890493ae3991a2e344dd0bb75442 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 13:23:58 -0700 Subject: [PATCH 21/24] azure_devops: address bugbot review comments - triggers/utils: match build.complete result case-insensitively, accept stopped/cancelled in addition to failed/canceled/partiallySucceeded so PascalCase and legacy Azure DevOps payloads aren't dropped - get_work_items_batch: chunk comma-separated IDs into 200-batch loops with proper status checks (was failing or returning incomplete data on >200 IDs) - Add tests for both behaviors Co-Authored-By: Claude Opus 4.7 --- .../tools/azure_devops/azure-devops.test.ts | 79 ++++++++++++++++++- .../azure_devops/get_work_items_batch.ts | 65 +++++++++++++-- apps/sim/tools/azure_devops/types.ts | 1 + apps/sim/triggers/azure_devops/utils.ts | 10 ++- 4 files changed, 144 insertions(+), 11 deletions(-) diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index f773fe4ce6..cd0eb7f2c2 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { afterEach, describe, expect, it, vi } from 'vitest' +import { isAzureDevOpsEventMatch } from '@/triggers/azure_devops/utils' import { tools } from '../registry' import type { ToolConfig } from '../types' import { addCommentTool } from './add_comment' @@ -572,9 +573,11 @@ describe('Azure DevOps response transforms', () => { }) const batch = await getWorkItemsBatchTool.transformResponse!( - responseJson({ value: [rawWorkItem] }) + responseJson({ value: [rawWorkItem] }), + { ...baseParams, ids: '101' } satisfies GetWorkItemsBatchParams ) expect(batch.output.metadata.count).toBe(1) + expect(batch.output.metadata.totalRequested).toBe(1) }) it('hydrates WIQL query results in chunks of 200 IDs', async () => { @@ -602,6 +605,26 @@ describe('Azure DevOps response transforms', () => { expect(result.output.metadata.workItems).toHaveLength(2) }) + it('chunks Get Work Items Batch requests larger than 200 IDs', async () => { + const fetchMock = vi + .fn() + .mockImplementation(() => Promise.resolve(responseJson({ value: [rawWorkItem] }))) + globalThis.fetch = fetchMock as unknown as typeof fetch + + const ids = Array.from({ length: 350 }, (_, i) => String(i + 1)).join(',') + + const result = await getWorkItemsBatchTool.transformResponse!( + responseJson({ value: [rawWorkItem] }), + { ...baseParams, ids } satisfies GetWorkItemsBatchParams + ) + + expect(fetchMock).toHaveBeenCalledTimes(1) + const followupChunk = new URL(String(fetchMock.mock.calls[0][0])) + expect(followupChunk.searchParams.get('ids')?.split(',')).toHaveLength(150) + expect(result.output.metadata.totalRequested).toBe(350) + expect(result.output.metadata.workItems).toHaveLength(2) + }) + it('throws when WIQL hydration fetch returns a non-OK status', async () => { const fetchMock = vi .fn() @@ -672,3 +695,57 @@ describe('Azure DevOps response transforms', () => { }) }) }) + +describe('Azure DevOps trigger event matching', () => { + const baseBuild = { eventType: 'build.complete' } + const baseWorkItem = { eventType: 'workitem.created' } + + it('matches build.complete results case-insensitively including stopped/Failed/Canceled', () => { + for (const result of [ + 'failed', + 'Failed', + 'FAILED', + 'canceled', + 'Canceled', + 'cancelled', + 'Cancelled', + 'stopped', + 'Stopped', + 'partiallySucceeded', + 'PartiallySucceeded', + ]) { + expect( + isAzureDevOpsEventMatch('azure_devops_build_failed', { + ...baseBuild, + resource: { result }, + }) + ).toBe(true) + } + }) + + it('does not match successful build.complete payloads', () => { + for (const result of ['succeeded', 'Succeeded', 'inProgress']) { + expect( + isAzureDevOpsEventMatch('azure_devops_build_failed', { + ...baseBuild, + resource: { result }, + }) + ).toBe(false) + } + }) + + it('ignores non-build event types when expecting build.complete', () => { + expect( + isAzureDevOpsEventMatch('azure_devops_build_failed', { + eventType: 'workitem.created', + resource: { result: 'failed' }, + }) + ).toBe(false) + }) + + it('matches workitem.created and passes through generic webhook', () => { + expect(isAzureDevOpsEventMatch('azure_devops_work_item_created', baseWorkItem)).toBe(true) + expect(isAzureDevOpsEventMatch('azure_devops_work_item_created', baseBuild)).toBe(false) + expect(isAzureDevOpsEventMatch('azure_devops_webhook', { eventType: 'anything' })).toBe(true) + }) +}) diff --git a/apps/sim/tools/azure_devops/get_work_items_batch.ts b/apps/sim/tools/azure_devops/get_work_items_batch.ts index 86de8de333..ef713a468d 100644 --- a/apps/sim/tools/azure_devops/get_work_items_batch.ts +++ b/apps/sim/tools/azure_devops/get_work_items_batch.ts @@ -12,7 +12,7 @@ export const getWorkItemsBatchTool: ToolConfig { + const allIds = params.ids + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + const firstChunk = allIds.slice(0, 200) const url = new URL( `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems` ) - url.searchParams.set('ids', params.ids) + url.searchParams.set('ids', firstChunk.join(',')) url.searchParams.set('$expand', 'all') url.searchParams.set('api-version', '7.2-preview.3') return url.toString() @@ -60,22 +65,61 @@ export const getWorkItemsBatchTool: ToolConfig { - const data = await response.json() - const workItems: AzureDevOpsWorkItem[] = (data.value ?? []).map( + transformResponse: async (response, params) => { + const firstData = await response.json() + const workItems: AzureDevOpsWorkItem[] = (firstData.value ?? []).map( (raw: AzureDevOpsRawWorkItem) => mapWorkItem(raw) ) + const allIds = params!.ids + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + + if (allIds.length > 200) { + const BATCH_SIZE = 200 + const organization = params!.organization.trim() + const project = params!.project.trim() + const authHeader = `Basic ${btoa(`:${params!.accessToken}`)}` + + for (let i = BATCH_SIZE; i < allIds.length; i += BATCH_SIZE) { + const chunk = allIds.slice(i, i + BATCH_SIZE) + const detailsUrl = new URL( + `https://dev.azure.com/${organization}/${project}/_apis/wit/workitems` + ) + detailsUrl.searchParams.set('ids', chunk.join(',')) + detailsUrl.searchParams.set('$expand', 'all') + detailsUrl.searchParams.set('api-version', '7.2-preview.3') + + const chunkResponse = await fetch(detailsUrl.toString(), { + method: 'GET', + headers: { 'Content-Type': 'application/json', Authorization: authHeader }, + }) + + if (!chunkResponse.ok) { + const errorBody = await chunkResponse.text().catch(() => '') + throw new Error( + `Failed to fetch work item batch chunk (${chunkResponse.status}): ${errorBody || chunkResponse.statusText}` + ) + } + + const chunkData = await chunkResponse.json() + for (const raw of chunkData.value ?? []) { + workItems.push(mapWorkItem(raw as AzureDevOpsRawWorkItem)) + } + } + } + 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')}` + : `Found ${workItems.length} work item(s) (of ${allIds.length} requested):\n\n${workItems.map(formatWorkItem).join('\n\n')}` return { success: true, output: { content, - metadata: { count: workItems.length, workItems }, + metadata: { count: workItems.length, totalRequested: allIds.length, workItems }, }, } }, @@ -90,6 +134,11 @@ export const getWorkItemsBatchTool: ToolConfig | undefined - const result = resource?.result as string | undefined - return result === 'failed' || result === 'canceled' || result === 'partiallySucceeded' + const result = (resource?.result as string | undefined)?.toLowerCase() + return ( + result === 'failed' || + result === 'canceled' || + result === 'cancelled' || + result === 'stopped' || + result === 'partiallysucceeded' + ) } if (triggerId === 'azure_devops_work_item_created') { From cdc80413de3b115f6d58df11df05b51b23c9c318 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 13:41:32 -0700 Subject: [PATCH 22/24] azure_devops: address additional bugbot comments - Block update_work_item now forwards areaPath; the Area Path subblock condition expanded to include update operation - get_build_timeline.failedRecords now also flags partiallySucceeded and succeededWithIssues, normalized case-insensitively. Output description and added a focused test Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/azure_devops.ts | 6 +++++- .../sim/tools/azure_devops/azure-devops.test.ts | 17 +++++++++++++++++ .../tools/azure_devops/get_build_timeline.ts | 10 ++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/azure_devops.ts b/apps/sim/blocks/blocks/azure_devops.ts index 970fe914bf..7af9bb2ffe 100644 --- a/apps/sim/blocks/blocks/azure_devops.ts +++ b/apps/sim/blocks/blocks/azure_devops.ts @@ -396,7 +396,10 @@ export const AzureDevOpsBlock: BlockConfig = { title: 'Area Path', type: 'short-input', placeholder: 'e.g. MyProject\\Team', - condition: { field: 'operation', value: 'azure_devops_create_work_item' }, + condition: { + field: 'operation', + value: ['azure_devops_create_work_item', 'azure_devops_update_work_item'], + }, mode: 'advanced', }, { @@ -533,6 +536,7 @@ export const AzureDevOpsBlock: BlockConfig = { remainingWork: params.remainingWork ? Number(params.remainingWork) : undefined, completedWork: params.completedWork ? Number(params.completedWork) : undefined, description: (params.description as string) || undefined, + areaPath: (params.areaPath as string) || undefined, tags: (params.tags as string) || undefined, } case 'azure_devops_add_comment': diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index cd0eb7f2c2..6a08ce3573 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -8,6 +8,7 @@ import type { ToolConfig } from '../types' import { addCommentTool } from './add_comment' import { createWorkItemTool } from './create_work_item' import { getBuildLogTool } from './get_build_log' +import { getBuildTimelineTool } from './get_build_timeline' import { getCommentsTool } from './get_comments' import { getPipelineTool } from './get_pipeline' import { getPipelineRunTool } from './get_pipeline_run' @@ -743,6 +744,22 @@ describe('Azure DevOps trigger event matching', () => { ).toBe(false) }) + it('build timeline includes partiallySucceeded and succeededWithIssues in failedRecords', async () => { + const records = [ + { id: 'a', name: 'Step A', type: 'Task', result: 'succeeded', log: { id: 1 } }, + { id: 'b', name: 'Step B', type: 'Task', result: 'failed', log: { id: 2 } }, + { id: 'c', name: 'Step C', type: 'Task', result: 'partiallySucceeded', log: { id: 3 } }, + { id: 'd', name: 'Step D', type: 'Task', result: 'succeededWithIssues', log: { id: 4 } }, + { id: 'e', name: 'Step E', type: 'Task', result: 'skipped', log: null }, + ] + const result = await getBuildTimelineTool.transformResponse!( + new Response(JSON.stringify({ records })) + ) + const failedIds = result.output.metadata.failedRecords.map((r) => r.id) + expect(failedIds).toEqual(['b', 'c', 'd']) + expect(result.output.metadata.failedCount).toBe(3) + }) + it('matches workitem.created and passes through generic webhook', () => { expect(isAzureDevOpsEventMatch('azure_devops_work_item_created', baseWorkItem)).toBe(true) expect(isAzureDevOpsEventMatch('azure_devops_work_item_created', baseBuild)).toBe(false) diff --git a/apps/sim/tools/azure_devops/get_build_timeline.ts b/apps/sim/tools/azure_devops/get_build_timeline.ts index ec14df7ac1..fbb10bdbaa 100644 --- a/apps/sim/tools/azure_devops/get_build_timeline.ts +++ b/apps/sim/tools/azure_devops/get_build_timeline.ts @@ -75,7 +75,12 @@ export const getBuildTimelineTool: ToolConfig r.result === 'failed') + const failedRecords = records.filter((r) => { + const result = r.result?.toLowerCase() + return ( + result === 'failed' || result === 'partiallysucceeded' || result === 'succeededwithissues' + ) + }) const content = failedRecords.length === 0 @@ -136,7 +141,8 @@ export const getBuildTimelineTool: ToolConfig Date: Tue, 19 May 2026 13:55:49 -0700 Subject: [PATCH 23/24] azure_devops: address more bugbot comments - Webhook provider extractIdempotencyId returns null when subscriptionId or notificationId is missing/empty, preventing the literal "azure_devops:undefined:undefined" key from collapsing unrelated deliveries into duplicates - Get Work Items Batch validates that at least one non-empty ID is supplied before issuing the API request, throwing a clear error instead of hitting an empty ids= query - Tests cover both behaviors Co-Authored-By: Claude Opus 4.7 --- .../lib/webhooks/providers/azure-devops.ts | 9 ++++++++- .../tools/azure_devops/azure-devops.test.ts | 20 +++++++++++++++++++ .../azure_devops/get_work_items_batch.ts | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/azure-devops.ts b/apps/sim/lib/webhooks/providers/azure-devops.ts index 0ec670cb26..27f5b95403 100644 --- a/apps/sim/lib/webhooks/providers/azure-devops.ts +++ b/apps/sim/lib/webhooks/providers/azure-devops.ts @@ -48,7 +48,14 @@ export const azureDevOpsHandler: WebhookProviderHandler = { extractIdempotencyId(body: unknown): string | null { const obj = body as Record | null if (!obj) return null - return `azure_devops:${obj.subscriptionId}:${obj.notificationId}` + const subscriptionId = + typeof obj.subscriptionId === 'string' && obj.subscriptionId ? obj.subscriptionId : null + const notificationId = + typeof obj.notificationId === 'number' || typeof obj.notificationId === 'string' + ? String(obj.notificationId) + : null + if (!subscriptionId || !notificationId) return null + return `azure_devops:${subscriptionId}:${notificationId}` }, async formatInput({ body, webhook, requestId }: FormatInputContext): Promise { diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index 6a08ce3573..4b2c3220bf 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -606,6 +606,15 @@ describe('Azure DevOps response transforms', () => { expect(result.output.metadata.workItems).toHaveLength(2) }) + it('throws when Get Work Items Batch is invoked with no valid IDs', () => { + expect(() => + buildUrl(getWorkItemsBatchTool, { + ...baseParams, + ids: ' , , ', + } satisfies GetWorkItemsBatchParams) + ).toThrow(/requires at least one work item ID/) + }) + it('chunks Get Work Items Batch requests larger than 200 IDs', async () => { const fetchMock = vi .fn() @@ -765,4 +774,15 @@ describe('Azure DevOps trigger event matching', () => { expect(isAzureDevOpsEventMatch('azure_devops_work_item_created', baseBuild)).toBe(false) expect(isAzureDevOpsEventMatch('azure_devops_webhook', { eventType: 'anything' })).toBe(true) }) + + it('extractIdempotencyId returns null when subscriptionId or notificationId is missing', async () => { + const { azureDevOpsHandler } = await import('@/lib/webhooks/providers/azure-devops') + expect(azureDevOpsHandler.extractIdempotencyId!({})).toBeNull() + expect(azureDevOpsHandler.extractIdempotencyId!({ subscriptionId: 'sub-1' })).toBeNull() + expect(azureDevOpsHandler.extractIdempotencyId!({ notificationId: 42 })).toBeNull() + expect( + azureDevOpsHandler.extractIdempotencyId!({ subscriptionId: 'sub-1', notificationId: 42 }) + ).toBe('azure_devops:sub-1:42') + expect(azureDevOpsHandler.extractIdempotencyId!(null)).toBeNull() + }) }) diff --git a/apps/sim/tools/azure_devops/get_work_items_batch.ts b/apps/sim/tools/azure_devops/get_work_items_batch.ts index ef713a468d..3b5200d2af 100644 --- a/apps/sim/tools/azure_devops/get_work_items_batch.ts +++ b/apps/sim/tools/azure_devops/get_work_items_batch.ts @@ -49,6 +49,9 @@ export const getWorkItemsBatchTool: ToolConfig id.trim()) .filter(Boolean) + if (allIds.length === 0) { + throw new Error('Get Work Items Batch requires at least one work item ID.') + } const firstChunk = allIds.slice(0, 200) const url = new URL( `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems` From 5959ecb2bca34a1cd05334c9de548557b22b6826 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 19 May 2026 14:09:50 -0700 Subject: [PATCH 24/24] azure_devops: pin add_comment to documented api-version 7.0-preview.3 Microsoft's Add Comments docs only publish 7.0-preview.3 (the 7.2 view falls back to the 7.0 page). Get Comments stays on the documented 7.2-preview.4. Matches what's strictly in the Azure DevOps REST API reference rather than relying on undocumented version behavior. Co-Authored-By: Claude Opus 4.7 --- apps/sim/tools/azure_devops/add_comment.ts | 2 +- apps/sim/tools/azure_devops/azure-devops.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/azure_devops/add_comment.ts b/apps/sim/tools/azure_devops/add_comment.ts index 5d307f3daf..36630ffeea 100644 --- a/apps/sim/tools/azure_devops/add_comment.ts +++ b/apps/sim/tools/azure_devops/add_comment.ts @@ -51,7 +51,7 @@ export const addCommentTool: ToolConfig = const url = new URL( `https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/workitems/${Number(params.workItemId)}/comments` ) - url.searchParams.set('api-version', '7.2-preview.4') + url.searchParams.set('api-version', '7.0-preview.3') return url.toString() }, method: 'POST', diff --git a/apps/sim/tools/azure_devops/azure-devops.test.ts b/apps/sim/tools/azure_devops/azure-devops.test.ts index 4b2c3220bf..104232b18a 100644 --- a/apps/sim/tools/azure_devops/azure-devops.test.ts +++ b/apps/sim/tools/azure_devops/azure-devops.test.ts @@ -420,7 +420,7 @@ describe('Azure DevOps request builders', () => { } satisfies AddCommentParams expect(buildUrl(addCommentTool, addParams)).toBe( - 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101/comments?api-version=7.2-preview.4' + 'https://dev.azure.com/contoso/Fabrikam/_apis/wit/workitems/101/comments?api-version=7.0-preview.3' ) expect(buildBody(addCommentTool, addParams)).toEqual({ text: 'SimIntegrationTest markdown comment',