Skip to content

fix: resolve work item project via org-level fetch on type change (#294)#371

Open
akash2017sky wants to merge 5 commits into
mainfrom
fix/issue-294-change-work-item-type
Open

fix: resolve work item project via org-level fetch on type change (#294)#371
akash2017sky wants to merge 5 commits into
mainfrom
fix/issue-294-change-work-item-type

Conversation

@akash2017sky
Copy link
Copy Markdown
Collaborator

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:

  1. If the request body included project, use it directly.
  2. Otherwise call findProjectForWorkItem, which iterates every project the caller has access to and tries getWorkItem(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 return null and the route to surface a misleading Ticket not found 404. Because the front-end's handleDialogTypeChange only treats response.ok as success, the dropdown silently reverted and the user saw nothing change.

Fix

  • New AzureDevOpsService.getWorkItemByIdOrgLevel(workItemId) — hits https://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.
  • Type route now resolves projectName in this order:
    1. Body project (if a non-empty string) — cheap hint, used as-is.
    2. getWorkItemByIdOrgLevel — reads System.TeamProject off the response.
  • 404 vs 500 now carry actionable detail (Ticket {id} not found in this organization vs Could not resolve the project for ticket {id}: {reason}) instead of a generic "Ticket not found".

The body's project becoming 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

  • Open a Task ticket on /tickets, change its type via the dialog dropdown — expect the type to update, no 404 in the console
  • Same on the detail page (/tickets/[id])
  • Pick a target type with required fields (e.g. Enhancement requiring Severity / Found By in the T-minus-15 template) — modal still appears, fields submit, type changes
  • Try changing type with the dev server cold (Turbopack first request) — no spurious 404 from a project iteration losing to a stale OAuth scope
  • Hit the route with a body that omits project (e.g. via curl) — still resolves correctly

Related

🤖 Generated with Claude Code

akash2017sky and others added 2 commits May 6, 2026 18:21
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>
@akash2017sky akash2017sky requested a review from EdiWeeks May 7, 2026 12:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 projectName via request body (if provided) or via the org-level fetch and System.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 thread src/lib/devops.ts Outdated
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 thread src/lib/devops.ts
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Unable to change work item type on an existing ticket

2 participants