Skip to content

feat(integrations): Azure DevOps REST client + URL parsing + PAT validation (PR 3/N)#7624

Closed
asaphko wants to merge 11 commits into
feat/azure-devops-02-modelsfrom
feat/azure-devops-03-client
Closed

feat(integrations): Azure DevOps REST client + URL parsing + PAT validation (PR 3/N)#7624
asaphko wants to merge 11 commits into
feat/azure-devops-02-modelsfrom
feat/azure-devops-03-client

Conversation

@asaphko
Copy link
Copy Markdown
Contributor

@asaphko asaphko commented May 28, 2026

Summary

PR 3 of the stacked Azure DevOps integration rollout. Stands up:

  • The ADO REST client's HTTP plumbing + list_projects (the only client function v1 needs). Status mapping: 401/403 → AzureDevOpsAuthError, 404 → AzureDevOpsNotFoundError, 5xx/other 4xx → requests.HTTPError. Basic auth with empty username + PAT as password; api-version=7.1 always in query; 10s timeout.
  • URL parsing for ADO pull-request and work-item URLs across cloud (dev.azure.com/{org}) and on-prem ({host}/{collection}) shapes. Returns NamedTuple refs; never raises on malformed input.
  • AzureDevOpsConfiguration.save() override that strips trailing slashes from organisation_url.
  • Serializer update() override that drops personal_access_token == WRITE_ONLY_PLACEHOLDER from validated data, preserving the stored PAT when the frontend round-trips the masked representation.
  • PAT validation in the configuration viewset: perform_create always validates against ADO; perform_update validates when the PAT actually changes (and isn't the placeholder). Auth failures → 400 with a clear message; 5xx/network failures → 400 with a "could not be reached, please retry" message (rather than 500).

Stack

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

Deliberately out of scope

  • Other client functions (list_repositories, list_pull_requests, list_work_items, comment-posting, label add/remove, subscription create/delete) — added incrementally by the consumer PRs that need them.
  • ADO 409 conflict mapping (will get a typed AzureDevOpsConflictError when subscriptions/labels need it, PR 4+).
  • Legacy visualstudio.com host shape and nested-collection on-prem URLs — current regex doesn't match these. The spec references them; we'll either extend the regex (and tests) in PR 4+ or drop the claim from the spec.
  • Metrics histogram for ADO API latency — lives in metrics.py later in the stack.

Notable deviations from the planned skeleton

  • perform_create adds an explicit self.get_queryset().exists() short-circuit before the ADO probe so the existing duplicate-config test doesn't need to mock the ADO call. Duplicates the parent class's check; a cleaner refactor (move uniqueness into the serializer's validate()) is a follow-up.
  • perform_update belt-and-braces the placeholder check (pat != WRITE_ONLY_PLACEHOLDER) alongside the serializer's update() pop — both layers correctly skip work on placeholder updates.
  • 5xx/network during validation now translates to 400 with a retry-friendly message (vs the plan's "propagate" wording).

Test plan

  • make lint clean
  • make typecheck clean
  • make test opts='-n0 tests/unit/integrations/azure_devops/' — 71 passed
  • 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' — 235 passed (adjacent-integration regression guard)
  • make django-make-migrations opts='--check --dry-run'No changes detected for all apps (PR 3 introduces no schema changes)

🤖 Generated with Claude Code

asaphko and others added 11 commits May 28, 2026 13:09
…gration

Third plan in the stacked-PRs rollout. Covers the Azure DevOps REST
client (limited to list_projects for v1), URL parsing for PR and
work-item URLs (cloud + on-prem), organisation_url trailing-slash
normalisation, serializer-side placeholder-PAT preservation on update,
and viewset PAT validation against ADO on create/update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add empty client/ and services/ packages plus a constants module holding
the request timeout and pinned REST API version (7.1). Subsequent
commits in this PR fill in the client modules and the URL-parsing
service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three classes: AzureDevOpsError (base), AzureDevOpsAuthError (401/403),
AzureDevOpsNotFoundError (404 on single-resource). Callers can catch
the base for "anything went wrong with ADO" or the specific subclass
for targeted handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two typed dicts for v1: AdoProject (id/name/url) and AdoProjectsPage
(results + continuation_token). Subsequent PRs will extend this module
with AdoRepository, AdoPullRequest, AdoWorkItem, AdoSubscription, and
the resource-metadata shape as their corresponding client functions
land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stand up the client's HTTP plumbing (Basic auth with empty username,
api-version=7.1 query param, 10s timeout) and the first concrete
function: list_projects. Status codes 401/403 map to AzureDevOpsAuthError,
404 to AzureDevOpsNotFoundError; 5xx and other 4xx bubble up as
requests.HTTPError. Subsequent client functions (repos, PRs, work items,
comments, labels, subscriptions) land in their respective consumer PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two NamedTuple types (AdoPullRequestRef, AdoWorkItemRef) and two parser
functions that handle both ADO cloud (dev.azure.com/{org}/...) and
Azure DevOps Server (host/{collection}/...) URL shapes. Parsing never
raises; unparseable URLs return None. Subsequent PRs (browse, comments,
webhook dispatcher) use these structured refs instead of raw URL string
comparisons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip trailing slashes from AzureDevOpsConfiguration.organisation_url
before persisting. This guarantees downstream URL composition
(client.api._ado_request) never produces double-slash paths regardless
of how the user supplied the URL.

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

Override the serializer's update() to drop personal_access_token from
validated_data when its value equals WRITE_ONLY_PLACEHOLDER. This lets
the frontend safely round-trip the masked representation without
overwriting the stored PAT, and keeps the imminent PAT-validation hook
in the viewset from validating a sentinel string against ADO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The configuration viewset now probes ADO with list_projects(top=1) when
the PAT is being set. On 401/403 ADO replies, the viewset returns 400
with a clear message and does not persist. On update, validation only
runs when the request actually changes the PAT (the serializer override
from the previous commit drops placeholder values).

Network and 5xx failures still propagate so transient unavailability is
not silently ignored. The existing tests are updated to mock the ADO
probe via the responses library; two new tests exercise the
invalid-PAT and placeholder-on-update paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three trailing-slash normalisation tests added in the previous
"normalise organisation_url on save" commit used 4 __-separated parts
(subject__qualifier__condition__outcome) which violates the repo's
FT003 lint rule. Rename to the canonical 3-part form using
organisation_url as the subject and "on_save" baked into the outcome.

Also commit the auto-regenerated events catalogue that lint produces
when the new azure_devops.configuration.* events are detected.

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

Also closes the diff-coverage gap on list_projects's continuation_token
request parameter.

Previously, if ADO replied 5xx during the create/update PAT probe (or
the request failed with a network error), the underlying
requests.HTTPError / requests.RequestException would bubble out and DRF
would return 500. Translate to a ValidationError with a "could not be
reached, please try again" message so the user gets an actionable 400.

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 Ready Ready Preview, Comment May 28, 2026 12:56pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ignored Ignored May 28, 2026 12:56pm
flagsmith-frontend-staging Ignored Ignored May 28, 2026 12:56pm

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 the Azure DevOps integration client, URL parsing services, and configuration validation. Specifically, it implements a REST client with a list_projects endpoint, URL parsing for pull requests and work items, automatic trailing slash normalization for organization URLs, and PAT validation during configuration creation and updates. Feedback on the changes highlights two critical issues in the configuration viewset: first, the PAT validation helper should catch generic exceptions to prevent unhandled 500 errors on malformed API responses; second, validation should also run when the organization URL is updated, even if the PAT remains unchanged, to prevent saving mismatched configurations.

Comment on lines +34 to +38
except requests.RequestException:
raise ValidationError(
"Azure DevOps could not be reached to validate the credentials. "
"Please try again."
) from None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The validation helper intends to handle malformed responses as well, but it only catches requests.RequestException. If Azure DevOps (or an intermediate proxy/firewall) returns a 200 OK with an unexpected payload (e.g., HTML or invalid JSON), list_projects will raise ValueError, KeyError, TypeError, or AttributeError. These exceptions do not inherit from RequestException and will cause an unhandled 500 Internal Server Error. Catching Exception ensures all failure modes are safely translated into a validation error.

Suggested change
except requests.RequestException:
raise ValidationError(
"Azure DevOps could not be reached to validate the credentials. "
"Please try again."
) from None
except Exception:
raise ValidationError(
"Azure DevOps could not be reached to validate the credentials. "
"Please try again."
) from None

Comment on lines +68 to 80
pat = serializer.validated_data.get("personal_access_token")
# Treat the masked placeholder as "no change" — the serializer drops
# it during ``update`` so it never reaches the database, and there is
# nothing meaningful to validate against ADO either.
if pat is not None and pat != WRITE_ONLY_PLACEHOLDER:
_validate_pat_against_ado(
organisation_url=serializer.validated_data.get(
"organisation_url",
serializer.instance.organisation_url, # type: ignore[union-attr]
),
pat=pat,
)
super().perform_update(serializer)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If a user updates the organisation_url but does not change the personal access token (meaning pat is None or WRITE_ONLY_PLACEHOLDER), the validation is skipped entirely. This allows saving an invalid or mismatched organization URL without verifying that the existing PAT has access to it. We should perform validation if either the PAT or the organization URL is updated.

Suggested change
pat = serializer.validated_data.get("personal_access_token")
# Treat the masked placeholder as "no change" — the serializer drops
# it during ``update`` so it never reaches the database, and there is
# nothing meaningful to validate against ADO either.
if pat is not None and pat != WRITE_ONLY_PLACEHOLDER:
_validate_pat_against_ado(
organisation_url=serializer.validated_data.get(
"organisation_url",
serializer.instance.organisation_url, # type: ignore[union-attr]
),
pat=pat,
)
super().perform_update(serializer)
organisation_url = serializer.validated_data.get("organisation_url")
pat = serializer.validated_data.get("personal_access_token")
is_pat_updated = pat is not None and pat != WRITE_ONLY_PLACEHOLDER
is_url_updated = (
organisation_url is not None
and organisation_url != serializer.instance.organisation_url # type: ignore[union-attr]
)
if is_pat_updated or is_url_updated:
validation_url = organisation_url or serializer.instance.organisation_url # type: ignore[union-attr]
validation_pat = (
pat if is_pat_updated else serializer.instance.personal_access_token # type: ignore[union-attr]
)
_validate_pat_against_ado(
organisation_url=validation_url,
pat=validation_pat,
)
super().perform_update(serializer)

@asaphko asaphko self-assigned this May 28, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.53%. Comparing base (c26daeb) to head (b24ca9c).

Additional details and impacted files
@@                      Coverage Diff                      @@
##           feat/azure-devops-02-models    #7624    +/-   ##
=============================================================
  Coverage                        98.52%   98.53%            
=============================================================
  Files                             1454     1464    +10     
  Lines                            55018    55312   +294     
=============================================================
+ Hits                             54209    54503   +294     
  Misses                             809      809            

☔ 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