Skip to content

Commit c084b6c

Browse files
waleedlatif1claude
andcommitted
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 <noreply@anthropic.com>
1 parent d11aa83 commit c084b6c

5 files changed

Lines changed: 95 additions & 34 deletions

File tree

apps/docs/content/docs/en/tools/azure_devops.mdx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,26 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
77

88
<BlockInfoCard
99
type="azure_devops"
10-
color="#FFFFFF"
10+
color="#0078D4"
1111
/>
1212

13+
{/* MANUAL-CONTENT-START:intro */}
14+
[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.
15+
16+
With the Azure DevOps integration in Sim, you can:
17+
18+
- **Inspect pipelines and runs**: List pipelines, fetch metadata, and walk through run history with status and result
19+
- **Triage build failures**: Pull build timelines to see which stage, job, or task failed, then fetch the exact log for the failing step
20+
- **Audit changes between builds**: Surface the work items that landed between any two builds — useful for release notes and regression hunts
21+
- **Query work items with WIQL**: Run full WIQL queries and get hydrated work item fields back in a single call, not just IDs
22+
- **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
23+
- **Collaborate via comments**: Add internal or public comments to work items and read full comment history
24+
- **React in real time**: Trigger workflows when builds fail or new work items are created via Azure DevOps service hooks
25+
26+
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.
27+
{/* MANUAL-CONTENT-END */}
28+
29+
1330
## Usage Instructions
1431

1532
Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.

apps/sim/blocks/blocks/azure_devops.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const AzureDevOpsBlock: BlockConfig<AzureDevOpsResponse> = {
2222
category: 'tools',
2323
integrationType: IntegrationType.DeveloperTools,
2424
tags: ['ci-cd', 'project-management', 'version-control'],
25-
bgColor: '#FFFFFF',
25+
bgColor: '#0078D4',
2626
icon: AzureDevOpsIcon,
2727
authMode: AuthMode.ApiKey,
2828
triggerAllowed: true,
@@ -136,7 +136,11 @@ export const AzureDevOpsBlock: BlockConfig<AzureDevOpsResponse> = {
136136
required: true,
137137
condition: {
138138
field: 'operation',
139-
value: ['azure_devops_list_build_logs', 'azure_devops_get_build_log', 'azure_devops_get_build_timeline'],
139+
value: [
140+
'azure_devops_list_build_logs',
141+
'azure_devops_get_build_log',
142+
'azure_devops_get_build_timeline',
143+
],
140144
},
141145
},
142146
{

apps/sim/tools/azure_devops/azure-devops.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ describe('Azure DevOps request builders', () => {
317317
{ op: 'replace', path: '/fields/System.State', value: 'Doing' },
318318
{ op: 'replace', path: '/fields/Microsoft.VSTS.Scheduling.Effort', value: 5 },
319319
])
320-
expect(buildBody(updateWorkItemTool, { ...baseParams, workItemId: 101 })).toEqual([])
320+
expect(() => buildBody(updateWorkItemTool, { ...baseParams, workItemId: 101 })).toThrow(
321+
/requires at least one field/
322+
)
321323

322324
const createWithEffortParams = {
323325
...createParams,
@@ -575,8 +577,10 @@ describe('Azure DevOps response transforms', () => {
575577
expect(batch.output.metadata.count).toBe(1)
576578
})
577579

578-
it('hydrates WIQL query results with a second fetch and caps IDs at 200', async () => {
579-
const fetchMock = vi.fn().mockResolvedValue(responseJson({ value: [rawWorkItem] }))
580+
it('hydrates WIQL query results in chunks of 200 IDs', async () => {
581+
const fetchMock = vi
582+
.fn()
583+
.mockImplementation(() => Promise.resolve(responseJson({ value: [rawWorkItem] })))
580584
globalThis.fetch = fetchMock as unknown as typeof fetch
581585

582586
const workItems = Array.from({ length: 201 }, (_, index) => ({
@@ -589,9 +593,27 @@ describe('Azure DevOps response transforms', () => {
589593
wiqlQuery: 'SELECT [System.Id] FROM workitems',
590594
} satisfies QueryWorkItemsParams)
591595

592-
const detailsUrl = new URL(String(fetchMock.mock.calls[0][0]))
593-
expect(detailsUrl.searchParams.get('ids')?.split(',')).toHaveLength(200)
594-
expect(result.output.metadata.workItems).toHaveLength(1)
596+
expect(fetchMock).toHaveBeenCalledTimes(2)
597+
const firstChunk = new URL(String(fetchMock.mock.calls[0][0]))
598+
const secondChunk = new URL(String(fetchMock.mock.calls[1][0]))
599+
expect(firstChunk.searchParams.get('ids')?.split(',')).toHaveLength(200)
600+
expect(secondChunk.searchParams.get('ids')?.split(',')).toHaveLength(1)
601+
expect(result.output.metadata.totalMatched).toBe(201)
602+
expect(result.output.metadata.workItems).toHaveLength(2)
603+
})
604+
605+
it('throws when WIQL hydration fetch returns a non-OK status', async () => {
606+
const fetchMock = vi
607+
.fn()
608+
.mockResolvedValue(new Response('forbidden', { status: 403, statusText: 'Forbidden' }))
609+
globalThis.fetch = fetchMock as unknown as typeof fetch
610+
611+
await expect(
612+
queryWorkItemsTool.transformResponse!(responseJson({ workItems: [{ id: 1, url: 'x' }] }), {
613+
...baseParams,
614+
wiqlQuery: 'SELECT [System.Id] FROM workitems',
615+
} satisfies QueryWorkItemsParams)
616+
).rejects.toThrow(/Failed to hydrate work item details/)
595617
})
596618

597619
it('does not hydrate WIQL empty results', async () => {

apps/sim/tools/azure_devops/query_work_items.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const queryWorkItemsTool: ToolConfig<QueryWorkItemsParams, QueryWorkItems
4444

4545
request: {
4646
url: (params) =>
47-
`https://dev.azure.com/${params.organization}/${params.project}/_apis/wit/wiql?api-version=7.2-preview.2`,
47+
`https://dev.azure.com/${params.organization.trim()}/${params.project.trim()}/_apis/wit/wiql?api-version=7.2-preview.2`,
4848
method: 'POST',
4949
headers: (params) => ({
5050
'Content-Type': 'application/json',
@@ -67,41 +67,53 @@ export const queryWorkItemsTool: ToolConfig<QueryWorkItemsParams, QueryWorkItems
6767
}
6868
}
6969

70-
const ids = workItemRefs
71-
.slice(0, 200)
72-
.map((wi) => wi.id)
73-
.join(',')
70+
const allIds = workItemRefs.map((wi) => wi.id)
71+
const BATCH_SIZE = 200
72+
const organization = params!.organization.trim()
73+
const project = params!.project.trim()
74+
const authHeader = `Basic ${btoa(`:${params!.accessToken}`)}`
7475

75-
const detailsUrl = new URL(
76-
`https://dev.azure.com/${params!.organization}/${params!.project}/_apis/wit/workitems`
77-
)
78-
detailsUrl.searchParams.set('ids', ids)
79-
detailsUrl.searchParams.set('$expand', 'all')
80-
detailsUrl.searchParams.set('api-version', '7.2-preview.3')
76+
const workItems: AzureDevOpsWorkItem[] = []
77+
for (let i = 0; i < allIds.length; i += BATCH_SIZE) {
78+
const chunk = allIds.slice(i, i + BATCH_SIZE)
79+
const detailsUrl = new URL(
80+
`https://dev.azure.com/${organization}/${project}/_apis/wit/workitems`
81+
)
82+
detailsUrl.searchParams.set('ids', chunk.join(','))
83+
detailsUrl.searchParams.set('$expand', 'all')
84+
detailsUrl.searchParams.set('api-version', '7.2-preview.3')
8185

82-
const detailsResponse = await fetch(detailsUrl.toString(), {
83-
method: 'GET',
84-
headers: {
85-
'Content-Type': 'application/json',
86-
Authorization: `Basic ${btoa(`:${params!.accessToken}`)}`,
87-
},
88-
})
86+
const detailsResponse = await fetch(detailsUrl.toString(), {
87+
method: 'GET',
88+
headers: {
89+
'Content-Type': 'application/json',
90+
Authorization: authHeader,
91+
},
92+
})
8993

90-
const detailsData = await detailsResponse.json()
91-
const workItems: AzureDevOpsWorkItem[] = (detailsData.value ?? []).map(
92-
(raw: AzureDevOpsRawWorkItem) => mapWorkItem(raw)
93-
)
94+
if (!detailsResponse.ok) {
95+
const errorBody = await detailsResponse.text().catch(() => '')
96+
throw new Error(
97+
`Failed to hydrate work item details (${detailsResponse.status}): ${errorBody || detailsResponse.statusText}`
98+
)
99+
}
100+
101+
const detailsData = await detailsResponse.json()
102+
for (const raw of detailsData.value ?? []) {
103+
workItems.push(mapWorkItem(raw as AzureDevOpsRawWorkItem))
104+
}
105+
}
94106

95107
const content =
96108
workItems.length === 0
97109
? 'No work item details found.'
98-
: `Found ${workItems.length} work item(s):\n\n${workItems.map(formatWorkItem).join('\n\n')}`
110+
: `Found ${workItems.length} work item(s) (of ${allIds.length} matched):\n\n${workItems.map(formatWorkItem).join('\n\n')}`
99111

100112
return {
101113
success: true,
102114
output: {
103115
content,
104-
metadata: { count: workItems.length, workItems },
116+
metadata: { count: workItems.length, totalMatched: allIds.length, workItems },
105117
},
106118
}
107119
},
@@ -115,7 +127,12 @@ export const queryWorkItemsTool: ToolConfig<QueryWorkItemsParams, QueryWorkItems
115127
type: 'object',
116128
description: 'Work items metadata',
117129
properties: {
118-
count: { type: 'number', description: 'Number of work items returned' },
130+
count: { type: 'number', description: 'Number of work items returned (after hydration)' },
131+
totalMatched: {
132+
type: 'number',
133+
description: 'Total number of work items matched by the WIQL query before hydration',
134+
optional: true,
135+
},
119136
workItems: {
120137
type: 'array',
121138
description: 'Array of work item details',

apps/sim/tools/azure_devops/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export interface QueryWorkItemsResponse extends ToolResponse {
274274
content: string
275275
metadata: {
276276
count: number
277+
totalMatched?: number
277278
workItems: AzureDevOpsWorkItem[]
278279
}
279280
}

0 commit comments

Comments
 (0)