feat(integrations): Azure DevOps labels service + tasks (PR 7/N)#7632
feat(integrations): Azure DevOps labels service + tasks (PR 7/N)#7632asaphko wants to merge 5 commits into
Conversation
…gration Seventh plan in the stacked-PRs rollout. Covers the labels service: client functions for ADO PR labels and work-item tags (the latter via read-modify-write on the System.Tags field), the services module with apply/remove flagsmith-label functions gated by labeling_enabled, and two task wrappers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
add_tag_to_pull_request POSTs to /_apis/git/pullrequests/{id}/labels
(project-scoped form — no repository GUID required). Idempotent on the
ADO side.
remove_tag_from_pull_request DELETEs and swallows 404 (label already
gone is the desired terminal state).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADO work items expose tags via a single System.Tags field (semicolon-separated string), not via a dedicated API. add_tag_to_work_item and remove_tag_from_work_item implement the GET-then-PATCH dance with two private helpers (_get_work_item_tags, _patch_work_item_tags). Both public functions are idempotent — they no-op when the desired terminal state already holds. Also generalises _ado_request to accept a content_type kwarg (ADO requires "application/json-patch+json" for work-item PATCH bodies) and widens json_body's typing to accept list bodies (JSON Patch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two public functions: - apply_flagsmith_label_to_resource(resource) parses the URL, dispatches to the right client function (PR labels API or work-item Tags PATCH), and applies the "flagsmith" tag. - remove_flagsmith_label_from_resource(...) takes fields directly so it can be called from the unlink task after the FER row is gone. Both are gated by labeling_enabled and never raise — requests.RequestException is caught and logged as label.apply_failed / label.removal_failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two @register_task_handler() decorated wrappers: - apply_azure_devops_label(resource_id): loads the FER and forwards to apply_flagsmith_label_to_resource. - remove_azure_devops_label(project_id, resource_url, resource_type): takes fields directly (FER may be gone by run time). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
There was a problem hiding this comment.
Code Review
This pull request introduces the Azure DevOps labeling layer, adding client functions, a service module, and task wrappers to apply and remove the 'flagsmith' label or tag on pull requests and work items. The feedback recommends optimizing database queries by using 'select_related' when fetching configurations and external resources, and improving error handling by explicitly catching custom 'AzureDevOpsError' exceptions to prevent task processor crashes.
| from integrations.azure_devops.client import ( | ||
| add_tag_to_pull_request, | ||
| add_tag_to_work_item, | ||
| remove_tag_from_pull_request, | ||
| remove_tag_from_work_item, | ||
| ) |
There was a problem hiding this comment.
Import AzureDevOpsError to allow catching custom client exceptions when applying or removing labels, preventing unhandled exceptions from crashing the task processor.
| from integrations.azure_devops.client import ( | |
| add_tag_to_pull_request, | |
| add_tag_to_work_item, | |
| remove_tag_from_pull_request, | |
| remove_tag_from_work_item, | |
| ) | |
| from integrations.azure_devops.client import ( | |
| AzureDevOpsError, | |
| add_tag_to_pull_request, | |
| add_tag_to_work_item, | |
| remove_tag_from_pull_request, | |
| remove_tag_from_work_item, | |
| ) |
| config: AzureDevOpsConfiguration | None = AzureDevOpsConfiguration.objects.filter( | ||
| project_id=project_id | ||
| ).first() |
There was a problem hiding this comment.
Use select_related("project") to prefetch the project relation. This avoids an additional database query when accessing config.project.organisation_id later in the logging context binding.
| config: AzureDevOpsConfiguration | None = AzureDevOpsConfiguration.objects.filter( | |
| project_id=project_id | |
| ).first() | |
| config: AzureDevOpsConfiguration | None = AzureDevOpsConfiguration.objects.select_related("project").filter( | |
| project_id=project_id | |
| ).first() |
| tag=AZURE_DEVOPS_FLAGSMITH_LABEL, | ||
| ) | ||
| log.info("label.applied", ado__resource__id=work_ref.work_item_id) | ||
| except requests.RequestException: |
There was a problem hiding this comment.
Catch AzureDevOpsError in addition to requests.RequestException. Since custom exceptions like AzureDevOpsAuthError and AzureDevOpsNotFoundError are raised by the client and do not inherit from requests.RequestException, they must be explicitly caught to prevent task crashes.
| except requests.RequestException: | |
| except (requests.RequestException, AzureDevOpsError): |
| tag=AZURE_DEVOPS_FLAGSMITH_LABEL, | ||
| ) | ||
| log.info("label.removed", ado__resource__id=work_ref.work_item_id) | ||
| except requests.RequestException: |
There was a problem hiding this comment.
| Dispatched at link time. No-op if labelling is disabled. | ||
| """ | ||
| try: | ||
| resource = FeatureExternalResource.objects.get(id=resource_id) |
There was a problem hiding this comment.
Use select_related("feature") when fetching the FeatureExternalResource. This avoids a lazy-loading database query when apply_flagsmith_label_to_resource accesses resource.feature.project_id.
| resource = FeatureExternalResource.objects.get(id=resource_id) | |
| resource = FeatureExternalResource.objects.select_related("feature").get(id=resource_id) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/azure-devops-06-comments #7632 +/- ##
=================================================================
- Coverage 98.53% 98.53% -0.01%
=================================================================
Files 1476 1478 +2
Lines 56329 56559 +230
=================================================================
+ Hits 55504 55728 +224
- Misses 825 831 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
PR 7 of the stacked Azure DevOps integration rollout. Adds the Flagsmith → ADO labelling layer:
POST /_apis/git/pullrequests/{id}/labels(project-scoped, no repo GUID) andDELETEwith 404 swallowed for idempotency.System.Tagsfield (ADO's only mechanism — semicolon-separated string, no dedicated tag endpoint). Both add and remove no-op when the desired terminal state already holds._ado_requestgeneralised to accept acontent_typekwarg (ADO PATCH on work items requiresapplication/json-patch+json).json_bodywidened to accept JSON Patch list bodies.services/labels.pywithapply_flagsmith_label_to_resource(resource)andremove_flagsmith_label_from_resource(*, project_id, resource_url, resource_type). Both gated bylabeling_enabled, both catchrequests.RequestExceptionand log without raising.tasks.pygainsapply_azure_devops_labelandremove_azure_devops_label@register_task_handler()wrappers.Stack
Plan:
docs/superpowers/plans/2026-05-28-azure-devops-07-labels.md.Out of scope
vcs/services.pydispatcher wiring that queuesapply_azure_devops_label/remove_azure_devops_labelonFeatureExternalResourcelifecycle events — lands in a later PR.Test plan
make lintcleanmake typecheckcleanmake test opts='-n0 tests/unit/integrations/azure_devops/'— 219 passed (PR 6 baseline 198 + 27 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'— adjacent-integration regression guardmake django-make-migrations opts='--check --dry-run'— no drift (PR 7 introduces no schema changes)🤖 Generated with Claude Code