fix: resolve work item project via org-level fetch on type change (#294)#371
Open
akash2017sky wants to merge 5 commits into
Open
fix: resolve work item project via org-level fetch on type change (#294)#371akash2017sky wants to merge 5 commits into
akash2017sky wants to merge 5 commits into
Conversation
The type-change route's fallback path (when the request body omits \`project\`) was iterating every project the user has access to via \`findProjectForWorkItem\`, swallowing per-project errors with a bare \`catch\`. If any of those getWorkItem calls failed silently — an OAuth scope hiccup on a cached project, a transient 5xx — the lookup returned null and the route surfaced \"Ticket not found\" with a 404, even though the work item plainly existed. Replace the iteration with \`getWorkItemByIdOrgLevel\`, which hits Azure DevOps' org-level work-item endpoint (no project segment) once and reads \`System.TeamProject\` off the response. One Graph call instead of N, no silent-error sink, and 404 / 500 carry the actual reason. Side effect: the body's \`project\` is now treated as a hint, not the source of truth. If the front-end ever sends a stale or wrong project the route still resolves correctly. Fixes #294 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes failures when changing an existing ticket’s work item type by avoiding the previous “scan all projects” approach for resolving a work item’s owning project, replacing it with a single org-scoped work item fetch that Azure DevOps resolves server-side.
Changes:
- Added an org-level work item fetch helper (
getWorkItemByIdOrgLevel) to resolve work items without knowing the project. - Updated the ticket type-change route to resolve
projectNamevia request body (if provided) or via the org-level fetch andSystem.TeamProject. - Improved error responses for “not found” vs “lookup failed” during project resolution.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/lib/devops.ts |
Adds an org-scoped work item fetch method to avoid per-project iteration. |
src/app/api/devops/tickets/[id]/type/route.ts |
Switches type-change project resolution to prefer body project and otherwise derive it from an org-level work item fetch. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+65
to
+72
| // Resolve the project. Prefer the body field (cheap, no extra Graph call) | ||
| // but fall back to fetching the work item at the org level, which works | ||
| // regardless of which project owns it. This replaces the older | ||
| // findProjectForWorkItem iteration that was prone to silent 404s when one | ||
| // of the cached projects no longer permitted a getWorkItem call. | ||
| let projectName: string | undefined = | ||
| typeof project === 'string' && project.trim() ? project.trim() : undefined; | ||
|
|
Comment on lines
+65
to
+69
| // Resolve the project. Prefer the body field (cheap, no extra Graph call) | ||
| // but fall back to fetching the work item at the org level, which works | ||
| // regardless of which project owns it. This replaces the older | ||
| // findProjectForWorkItem iteration that was prone to silent 404s when one | ||
| // of the cached projects no longer permitted a getWorkItem call. |
Comment on lines
+498
to
+500
| async getWorkItemByIdOrgLevel(workItemId: number): Promise<DevOpsWorkItem | null> { | ||
| const response = await fetch( | ||
| `${this.baseUrl}/_apis/wit/workitems/${workItemId}?$expand=all&api-version=7.1`, |
Comment on lines
+503
to
+508
| if (response.status === 404) return null; | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Failed to fetch work item ${workItemId}: ${response.status} ${response.statusText}` | ||
| ); | ||
| } |
- Always resolve the owning project via getWorkItemByIdOrgLevel; remove the body `project` shortcut so a stale or wrong client value can no longer cause changeWorkItemType to target the wrong project (matches what the PR description originally promised). - Make getWorkItemByIdOrgLevel fields-selectable: the route now requests only `System.TeamProject` instead of `$expand=all`, dropping the payload to a single field. Default still falls back to `$expand=all` for any future caller that needs the full work item. - Include the DevOps response body (truncated to 500 chars) in non-404 error throws so permission-scope / 5xx failures surface with actionable detail instead of a bare status code. - Fix misleading "no extra Graph call" comment — this is the WIT API, not Graph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Type changes on existing tickets (
PATCH /api/devops/tickets/{id}/type) were failing with a 404 even when the ticket plainly existed. Replaces the brittle multi-project iteration with a single org-level work-item fetch.Fixes #294.
Root cause
The route's project resolver had two paths:
project, use it directly.findProjectForWorkItem, which iterates every project the caller has access to and triesgetWorkItem(project, id)on each.Path 2 used a bare
catch {}to skip projects that didn't own the work item. That catch also swallowed real failures — an OAuth scope hiccup on a cached project, a transient 5xx, anything that wasn't actually a 404 — leaving the iteration to returnnulland the route to surface a misleadingTicket not found404. Because the front-end'shandleDialogTypeChangeonly treatsresponse.okas success, the dropdown silently reverted and the user saw nothing change.Fix
AzureDevOpsService.getWorkItemByIdOrgLevel(workItemId)— hitshttps://dev.azure.com/{org}/_apis/wit/workitems/{id}?$expand=all&api-version=7.1. No project in the URL; Azure DevOps resolves it server-side. One call instead of N, and the 404 / non-OK paths surface real status codes and messages.projectNamein this order:project(if a non-empty string) — cheap hint, used as-is.getWorkItemByIdOrgLevel— readsSystem.TeamProjectoff the response.Ticket {id} not found in this organizationvsCould not resolve the project for ticket {id}: {reason}) instead of a generic "Ticket not found".The body's
projectbecoming a hint rather than the source of truth has a small bonus side effect: if the front-end ever sends a stale or wrong project, the route still routes the change to the correct project.Test plan
/tickets, change its type via the dialog dropdown — expect the type to update, no 404 in the console/tickets/[id])project(e.g. via curl) — still resolves correctlyRelated
🤖 Generated with Claude Code