Skip to content

feat(integrations): Azure DevOps browse endpoints (PR 5/N)#7629

Closed
asaphko wants to merge 9 commits into
feat/azure-devops-04-taggingfrom
feat/azure-devops-05-browse
Closed

feat(integrations): Azure DevOps browse endpoints (PR 5/N)#7629
asaphko wants to merge 9 commits into
feat/azure-devops-04-taggingfrom
feat/azure-devops-05-browse

Conversation

@asaphko
Copy link
Copy Markdown
Contributor

@asaphko asaphko commented May 28, 2026

Summary

PR 5 of the stacked Azure DevOps integration rollout. Extends the REST client with three new functions and exposes four paginated browse endpoints under /api/v1/projects/{flagsmith_project_id}/azure-devops/.

  • Client: list_repositories, list_pull_requests, list_work_items (WIQL + workitemsbatch orchestration). _ado_request generalised to handle both bare paths and project-scoped _apis/-bearing paths.
  • Browse endpoints: projects, repositories, pull-requests, work-items. Continuation-token pagination matches ADO's native shape. Shared _AdoListView base handles permissions, config lookup, and error mapping (400 no config / 404 unknown resource / 502 auth failure / 503 unreachable).
  • Browse serializers in a new serializers/browse.py (inheritance chain rooted at AdoBrowseQueryParamsSerializer). The existing AzureDevOpsConfigurationSerializer moves to serializers/__init__.py so all PR 2 imports keep working.

Stack

Plan: docs/superpowers/plans/2026-05-28-azure-devops-05-browse.md.

Spec deviations captured during implementation

  • PR title search not supported. ADO's GET /_apis/git/pullrequests exposes only searchCriteria.status — no text search. The PR-browse endpoint takes state= only. Work-item search via WIQL CONTAINS still works.
  • workitemsbatch is POST, not GET. The spec said GET; ADO's REST docs require POST. Implementing as POST.
  • Browse URL prefix is /{project_pk}/azure-devops/..., not /{project_pk}/integrations/azure-devops/... to avoid routing conflict with the CRUD viewset's {pk} capture. Matches GitLab's /{project_pk}/gitlab/projects/ precedent.
  • Pagination shape: {results, next, previous} with previous=None and no count. ADO continuation tokens don't reverse-paginate or expose totals. The next URL contains continuation_token=<token>.

Spec doc updated in commit bed431a51 to reflect these.

Notable implementation details

  • WIQL escaping: only single quotes need escaping (doubled per WIQL spec). Column names are hard-coded; only user-supplied values flow through _escape_wiql_string.
  • Work-item pagination: offset-based on the WIQL ID list. Re-issues the WIQL query on each page (concurrent edits to the ID set will silently shift offsets — acceptable for v1 typeahead-picker use).
  • continuation_token validation: AdoWorkItemsQueryParamsSerializer overrides the field to IntegerField(min_value=0) so negative offsets / non-integer strings are rejected at serializer-validation time (400) rather than crashing the view (500).

Caught + fixed during final review

  • Uncaught AzureDevOpsNotFoundError in the browse view → now maps to 404 with a structured detail. New test added.
  • Negative continuation_token=-1 previously leaked the wrong slice → now rejected by the serializer with 400. Two new serializer tests.
  • 502 auth-error branch had no test coverage → added.

Out of scope

  • ADO Code Search API (would enable PR title search).
  • Work-item type discovery endpoint (frontend will hardcode or accept free text in v1).
  • Frontend picker components (separate PR).
  • ADO request-duration histogram (lands with metrics.py).

Test plan

  • make lint clean
  • make typecheck clean
  • make test opts='-n0 tests/unit/integrations/azure_devops/' — 173 passed (PR 4 baseline 127 + 46 new)
  • make test opts='tests/unit/integrations/gitlab tests/unit/integrations/github tests/unit/features/test_unit_feature_external_resources_views.py tests/unit/features/test_migrations.py' — passing (adjacent-integration regression guard)
  • make django-make-migrations opts='--check --dry-run' — no drift (PR 5 introduces no schema changes)

🤖 Generated with Claude Code

asaphko and others added 9 commits May 28, 2026 14:35
…gration

Fifth plan in the stacked-PRs rollout. Covers the Azure DevOps browse
endpoints: extends the REST client with list_repositories,
list_pull_requests, and list_work_items (WIQL + workitemsbatch); adds
four ListAPIView subclasses under /api/v1/projects/{pk}/azure-devops/;
splits the serializers module into a subpackage with new browse
query-param serializers.

Spec deviations captured: ADO doesn't support PR title search; WIQL
single-quote escaping by doubling; workitemsbatch is POST (the spec
said GET). Browse URL prefix matches GitLab's "/{pk}/{vendor}/"
pattern to avoid routing conflict with the CRUD viewset's {pk}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new types covering the resource shapes the browse endpoints will
return: AdoRepository, AdoPullRequest, AdoPullRequestsPage, AdoWorkItem,
AdoWorkItemsPage. Same page-shape (results + continuation_token) as
AdoProjectsPage — ADO's REST API uses continuation tokens uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists git repositories within an ADO project. Generalises _ado_request
to accept paths that already contain "_apis/" (project-scoped
endpoints) alongside the existing bare-path shape. defaultBranch is
optional on the wire (ADO omits it for empty repos) so we default to
"" when absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists pull requests in an ADO project, filterable by state (active /
completed / abandoned / all). ADO's REST API doesn't expose a text
search for PRs, so this function takes state + paging only — title
search is a work-item-only capability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements work-item search via WIQL + workitemsbatch:
  1. POST /{project}/_apis/wit/wiql with a parameterised query for
     state, type, and title CONTAINS — returns matched IDs.
  2. POST /_apis/wit/workitemsbatch with the page-sized slice of IDs
     — returns rows with id/title/state/type/url.

Pagination is offset-based on the WIQL ID list (continuation_token is
the next offset as a stringified integer; None on the final page).
Single-quote escaping in CONTAINS uses WIQL's double-quote convention.
Column names are hard-coded; only user-supplied values are escaped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four DRF serializers covering paging + per-resource filter shapes:
- AdoBrowseQueryParamsSerializer: paging only (projects browse).
- AdoRepositoriesQueryParamsSerializer: + ado_project_id.
- AdoPullRequestsQueryParamsSerializer: + state (choice-validated).
- AdoWorkItemsQueryParamsSerializer: + search_text, state, work_item_type.

The existing AzureDevOpsConfigurationSerializer moves to
serializers/__init__.py so the `from integrations.azure_devops.serializers
import X` path keeps working for PR 2's viewset and tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four ListAPIView subclasses sharing a _AdoListView base for permission
handling, config lookup, error mapping (400 no config / 502 auth / 503
unreachable), and continuation-token paging. The next commit wires
their URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four nested paths under /api/v1/projects/{project_pk}/azure-devops/:
projects, repositories, pull-requests, work-items. Path prefix is
"azure-devops/" (not "integrations/azure-devops/") to mirror the
GitLab precedent and avoid routing conflict with the CRUD viewset's
{pk} capture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r 502 path

Three correctness fixes surfaced by the final whole-PR review:

1. _AdoListView.list now catches AzureDevOpsNotFoundError and returns
   404 with a structured detail. Previously, a bad ado_project_id
   produced a 500 via Django's default exception handler.
2. AdoWorkItemsQueryParamsSerializer overrides continuation_token to an
   IntegerField(min_value=0). The work-items client interprets the
   token as an integer offset into the WIQL ID list; negative values
   used to leak the wrong slice, and non-integer strings used to raise
   uncaught ValueErrors. The view stringifies the validated int back
   before passing to list_work_items.
3. New tests cover the 502 auth-error branch and the 404 not-found
   branch on the browse views, plus the two new serializer rejections.

Also updates the spec to match the implementation:
- Browse URL prefix /azure-devops/ (not /integrations/azure-devops/)
- list_pull_requests has no search_text (ADO doesn't support PR title
  search)
- workitemsbatch is POST (the spec said GET)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Building Building Preview May 28, 2026 2:17pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ignored Ignored May 28, 2026 2:17pm
flagsmith-frontend-staging Ignored Ignored May 28, 2026 2:17pm

Request Review

@github-actions github-actions Bot added api Issue related to the REST API docs Documentation updates feature New feature or request and removed docs Documentation updates labels May 28, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces browse endpoints for the Azure DevOps (ADO) integration, allowing users to list projects, repositories, pull requests, and work items. It extends the ADO client with new functions, implements a WIQL-based work item search with batch hydration, adds query parameter serializers, and wires up the corresponding views and URLs. A security review of the changes identified a potential path traversal vulnerability in the ado_project_id query parameter, which is interpolated directly into API request paths. It is recommended to add a regex validator to restrict slashes and backslashes in the project ID.

Comment on lines +1 to +12
from rest_framework import serializers

_PR_STATE_CHOICES = ("active", "completed", "abandoned", "all")


class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]):
top = serializers.IntegerField(default=100, min_value=1, max_value=200)
continuation_token = serializers.CharField(required=False, allow_blank=True)


class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer):
ado_project_id = serializers.CharField()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

Security Vulnerability: Path Traversal in ado_project_id

The ado_project_id query parameter is interpolated directly into the Azure DevOps API request paths (e.g., f"{ado_project_id}/_apis/git/repositories"). Since there is no validation on ado_project_id, an attacker with access to the browse endpoints could pass path traversal sequences (such as ../../) to manipulate the request URL. This would allow them to perform unauthorized API requests against other organizations or projects using the stored Personal Access Token (PAT).

Remediation:
Add a RegexValidator to ado_project_id in AdoRepositoriesQueryParamsSerializer to ensure it does not contain slashes (/) or backslashes (\\), which are forbidden in valid Azure DevOps project IDs and names anyway.

Suggested change
from rest_framework import serializers
_PR_STATE_CHOICES = ("active", "completed", "abandoned", "all")
class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]):
top = serializers.IntegerField(default=100, min_value=1, max_value=200)
continuation_token = serializers.CharField(required=False, allow_blank=True)
class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer):
ado_project_id = serializers.CharField()
from django.core.validators import RegexValidator
from rest_framework import serializers
_PR_STATE_CHOICES = ("active", "completed", "abandoned", "all")
class AdoBrowseQueryParamsSerializer(serializers.Serializer[None]):
top = serializers.IntegerField(default=100, min_value=1, max_value=200)
continuation_token = serializers.CharField(required=False, allow_blank=True)
class AdoRepositoriesQueryParamsSerializer(AdoBrowseQueryParamsSerializer):
ado_project_id = serializers.CharField(
validators=[
RegexValidator(
regex=r"^[^/\\\\]+$",
message="Invalid Azure DevOps project ID or name.",
)
]
)

@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

❌ Patch coverage is 99.48052% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.54%. Comparing base (6193e5e) to head (bed431a).

Files with missing lines Patch % Lines
api/integrations/azure_devops/client/api.py 95.91% 2 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                       @@
##           feat/azure-devops-04-tagging    #7629    +/-   ##
==============================================================
  Coverage                         98.54%   98.54%            
==============================================================
  Files                              1469     1472     +3     
  Lines                             55614    55992   +378     
==============================================================
+ Hits                              54803    55179   +376     
- Misses                              811      813     +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants