Files
kandev/docs/specs/azure-devops-integration/spec.md
T
2026-07-21 08:40:26 +01:00

286 lines
19 KiB
Markdown

---
status: building
created: 2026-07-17
owner: tbd
---
# Azure DevOps Integration
Decision: [ADR-2026-07-20-provider-neutral-remote-repositories](../../decisions/2026-07-20-provider-neutral-remote-repositories.md)
## Why
Teams whose source code and planning work live in Azure DevOps cannot use Kandev's
GitHub or GitLab browsing surfaces to find work items, inspect pull requests, or
associate a pull request with a task. Azure users must be able to connect their
workspace and read Azure Boards and Azure Repos data without installing or
authenticating the GitHub CLI.
## What
- An Azure DevOps connection is configured independently for each Kandev
workspace.
- The first release supports Azure DevOps Services organizations hosted at
`https://dev.azure.com/<organization>` and authenticates with a personal
access token stored in Kandev's encrypted secret store.
- Azure DevOps reads use the Azure DevOps REST API directly. Neither `gh` nor
`az` is required for connection checks, work-item reads, pull-request reads,
or pull-request synchronization.
- Users can test, replace, copy to another workspace, and delete an Azure DevOps
connection from Settings > Integrations > Azure DevOps.
- Users can browse work items returned by WIQL, inspect their core fields, and
launch the existing task-creation flow with the work-item title, description,
URL, project, type, state, and identifier available to the launcher.
- The work-item and pull-request browse surface leads with named, provider-aware
presets and supports workspace-scoped saved views. Raw WIQL remains available
in an Advanced section instead of occupying the primary filter surface.
- Users can browse active pull requests by project and repository, including
pull requests authored by them and pull requests where they are a reviewer.
- Pull-request detail includes branches, author, reviewers and votes, comment
threads, linked work items, and branch-policy evaluation status.
- A pull request can be associated with a Kandev task. The association survives
backend restarts and refreshes in the background without requiring the task's
agent environment to contain Azure or GitHub tooling.
- Azure DevOps failures are isolated from GitHub, GitLab, Jira, and other
integrations. An absent or invalid Azure connection does not prevent Kandev
from starting.
- Saving, copying, replacing, or deleting any integration configuration updates
integration navigation immediately. The 90-second health poll remains a
recovery mechanism, not the expected propagation path after a local mutation.
- Configured integrations show an Enabled status in the expanded workspace
settings navigation. Azure DevOps uses the official product mark consistently
in settings, browse, and task-creation surfaces.
- The task-creation repository picker combines repositories from every
configured source-control provider: GitHub, GitLab, and Azure DevOps. Users
can still paste a supported HTTPS or SSH repository URL manually. When more
than one repository provider is available, bottom tabs switch the visible
provider results; no provider tab bar is shown for a single provider. When
all three providers are available, compact icon tabs retain accessible names
and expose provider names on hover.
- Azure DevOps private repositories can be materialized with the workspace PAT
by the Kandev backend. The PAT is never added to task metadata, clone URLs,
agent environment variables, logs, or persisted repository rows. Push access
remains the responsibility of the selected executor's Git credentials.
- The Azure DevOps browse and settings surfaces provide equivalent desktop and
mobile workflows.
- Organization URL inputs accept an optional trailing slash and persist the
canonical URL without it.
- PAT setup instructions and the organization-specific token-settings link are
available from an info control beside the PAT field on hover, focus, or tap.
- Opening the Azure DevOps browser runs the default work-item query as soon as
the connected project's filters are ready; users do not need to submit the
initial search manually.
## Data Model
### `azure_devops_configs`
One row per workspace:
| Field | Type | Constraint |
| ---------------------- | -------- | -------------------------------------------------------------- |
| `workspace_id` | text | primary key |
| `organization_url` | text | required, canonical `https://dev.azure.com/<organization>` URL |
| `default_project_id` | text | optional project GUID |
| `default_project_name` | text | optional display name |
| `auth_method` | text | `pat` in the first release |
| `last_checked_at` | datetime | nullable |
| `last_ok` | boolean | required, default false |
| `last_error` | text | required, default empty |
| `created_at` | datetime | required |
| `updated_at` | datetime | required |
The PAT is never stored in SQLite. It is stored under the encrypted secret key
`azure_devops:<workspace_id>:pat`.
### `azure_devops_task_prs`
One row per task, repository, and Azure pull request:
| Field | Type | Constraint |
| --------------------- | -------- | --------------------------------------------------------------- |
| `id` | text | primary key UUID |
| `task_id` | text | required |
| `repository_id` | text | Kandev repository ID, required |
| `organization_url` | text | required |
| `project_id` | text | required |
| `azure_repository_id` | text | Azure repository GUID, required |
| `pull_request_id` | integer | required |
| `pull_request_url` | text | required |
| `title` | text | required |
| `source_branch` | text | required, normalized without `refs/heads/` for display |
| `target_branch` | text | required, normalized without `refs/heads/` for display |
| `author_id` | text | required |
| `author_name` | text | required |
| `status` | text | `active`, `completed`, or `abandoned` |
| `review_state` | text | normalized summary: `approved`, `waiting`, `rejected`, or empty |
| `policy_state` | text | normalized summary: `success`, `pending`, `failure`, or empty |
| `is_draft` | boolean | required |
| `last_synced_at` | datetime | nullable |
| `created_at` | datetime | required |
| `updated_at` | datetime | required |
The tuple `(task_id, repository_id, azure_repository_id, pull_request_id)` is
unique. Provider-native reviewer votes, threads, and policy records are fetched
on demand and are not flattened into GitHub review/check records.
### Repository provider fields
Azure repositories use the existing repository fields with
`provider = "azure_devops"`, the Azure repository GUID in `provider_repo_id`,
the project ID in `provider_owner`, and the repository name in `provider_name`.
Provider-backed repositories also persist the provider-returned canonical HTTPS
clone URL in `remote_url`. This avoids reconstructing URLs from GitHub-specific
owner/name assumptions and allows remote executors to address Azure organizations
and GitLab self-managed hosts correctly. Credentials are never embedded in this
field.
### Saved Azure views
Saved Azure views are workspace-scoped JSON records containing an ID, label,
kind (`work_item` or `pull_request`), provider-native query/filter values, and a
creation timestamp. Invalid entries are ignored when read. Saving a view never
persists result data or credentials.
## API Surface
Every route requires `workspace_id` as a query parameter unless the workspace
is present in the path.
| Method | Path | Behavior |
| -------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `GET` | `/api/v1/azure-devops/config` | Return redacted workspace configuration or 204 |
| `POST` | `/api/v1/azure-devops/config` | Validate and save organization, project, and optional replacement PAT |
| `DELETE` | `/api/v1/azure-devops/config` | Delete configuration and PAT |
| `POST` | `/api/v1/azure-devops/config/test` | Test submitted or stored credentials without persisting submitted values |
| `POST` | `/api/v1/azure-devops/config/copy` | Copy configuration and credential to another workspace |
| `GET` | `/api/v1/azure-devops/projects` | List accessible projects |
| `GET` | `/api/v1/azure-devops/repositories` | List repositories, optionally filtered by project |
| `GET` | `/api/v1/azure-devops/repositories/:projectId/:repositoryId/branches` | List repository branches for task creation |
| `GET` | `/api/v1/azure-devops/views` | Return workspace-scoped saved Azure views |
| `PUT` | `/api/v1/azure-devops/views` | Replace workspace-scoped saved Azure views |
| `POST` | `/api/v1/azure-devops/work-items/search` | Execute WIQL and return hydrated work items |
| `GET` | `/api/v1/azure-devops/work-items/:id` | Return one hydrated work item |
| `GET` | `/api/v1/azure-devops/pull-requests` | List PRs by project, repository, status, author, or reviewer |
| `GET` | `/api/v1/azure-devops/pull-requests/:projectId/:repositoryId/:pullRequestId` | Return PR detail |
| `GET` | `/api/v1/azure-devops/pull-requests/:projectId/:repositoryId/:pullRequestId/feedback` | Return reviewers, threads, linked work items, and policies |
| `GET` | `/api/v1/azure-devops/workspaces/:workspaceId/task-prs` | Return task PR associations grouped by task |
| `POST` | `/api/v1/azure-devops/tasks/:taskId/pull-requests` | Validate and associate an Azure PR with a task repository |
| `POST` | `/api/v1/azure-devops/tasks/:taskId/pull-requests/sync` | Refresh persisted state for one association |
Search requests contain `project`, `wiql`, and an optional `top` value. The
service hydrates WIQL references in batches no larger than 200. Descriptions
returned as HTML are sanitized before display.
Task repository inputs use the provider-neutral `remote_url` field. The legacy
`github_url` field remains accepted during migration and is normalized to the
same internal input. Provider metadata supplied by the browser is treated as a
hint and revalidated from the configured provider before persistence or clone.
## Permissions
- Any user who can configure a Kandev workspace can manage that workspace's
Azure DevOps connection under the same authorization model as Jira and
Linear configuration.
- The initial PAT requires read access to Work Items and Code. Kandev does not
request or exercise work-item write, thread write, or code write permissions
in this release.
- The backend may use the Code Read permission for a one-time authenticated Git
clone. It supplies the PAT through an ephemeral Git credential mechanism and
clears that mechanism when the child process exits.
- Credentials from one workspace must never be used to answer a request for a
different workspace.
## Failure Modes
- Missing workspace configuration returns a typed not-configured response and
a connection CTA; it does not invoke `gh` or `az`.
- A 401 or 403 marks the connection unhealthy and surfaces an authentication or
permission error without deleting the stored PAT.
- Rate limiting, timeouts, and Azure 5xx responses preserve the last known
health and PR association data while surfacing staleness and the current
error.
- Invalid organization URLs, unsupported hosts, missing workspace IDs, and
malformed WIQL are rejected without persistence.
- A WIQL result larger than one batch is hydrated in deterministic batches;
one omitted/deleted work item does not corrupt the rest of the page.
- PR association fails closed when the repository is not attached to the task
or is not an `azure_devops` repository.
- A repository selected from an integration is rejected if its canonical URL or
provider identity no longer matches data returned for the active workspace.
- Failure to resolve or use server-side Azure clone credentials fails the clone
without falling back to an unauthenticated URL containing a secret.
- Integration initialization errors are logged as non-fatal and the rest of
the backend remains available.
## Persistence Guarantees
- Configuration, connection health, encrypted PATs, and task PR associations
survive backend restarts.
- Browse results, PR feedback, and REST response caches are transient.
- Deleting a workspace follows the existing integration cleanup behavior and
removes its Azure configuration, PAT, and task PR associations.
## Scenarios
- **GIVEN** a workspace without GitHub CLI installed, **WHEN** a user saves a
valid Azure organization and PAT, **THEN** the connection succeeds and Azure
projects can be listed without executing `gh` or `az`.
- **GIVEN** two workspaces configured for different Azure organizations,
**WHEN** each workspace searches work items, **THEN** each response contains
only data accessible through that workspace's credential.
- **GIVEN** a valid WIQL query returning more than 200 references, **WHEN** a
user runs the query, **THEN** Kandev hydrates the requested page in batches
and returns normalized work items in query order.
- **GIVEN** a displayed Azure work item, **WHEN** a user launches a task from
it, **THEN** the task-creation flow is populated with the work-item context
and source URL.
- **GIVEN** a user is a reviewer on an active Azure PR, **WHEN** they select the
reviewer preset, **THEN** the PR appears with its repository, branches, draft
state, and current vote summary.
- **GIVEN** an Azure PR linked to a Kandev task, **WHEN** reviewer votes,
threads, or policy evaluations change upstream, **THEN** a refresh updates
the displayed summary while retaining Azure-native detail.
- **GIVEN** an expired PAT, **WHEN** the health poller checks the connection,
**THEN** settings shows the connection as unhealthy with a reconnect action
and existing PR associations remain stored.
- **GIVEN** a user saves or deletes an integration configuration, **WHEN** the
request succeeds, **THEN** settings status and home integration navigation
update without waiting for the periodic health poll.
- **GIVEN** multiple configured source-control providers, **WHEN** a user opens
the Remote repository picker, **THEN** a bottom tab is shown for each
available repository provider, only the active provider's matching results
are visible, and selections retain the correct provider icon and branch
source on desktop and mobile. The tab footer does not scroll vertically, and
three-provider tabs compact to icons with accessible provider names.
- **GIVEN** only one configured source-control provider, **WHEN** a user opens
the Remote repository picker, **THEN** its repositories are shown without a
provider tab bar.
- **GIVEN** a private Azure repository selected for a task, **WHEN** Kandev
materializes it, **THEN** the backend uses the workspace PAT for the clone and
no task or agent-visible value contains the PAT.
- **GIVEN** a user chooses an Azure preset or saved view, **WHEN** they search,
**THEN** Kandev applies the preset's provider-native query while Advanced WIQL
remains available for custom work-item searches.
- **GIVEN** a narrow mobile viewport, **WHEN** a user configures Azure DevOps or
browses work items and PRs, **THEN** all filters and primary actions remain
reachable without horizontal page scrolling.
## Out Of Scope
- Azure DevOps Server or Team Foundation Server installations.
- Microsoft Entra OAuth, service principals, and managed identities.
- Creating, updating, or transitioning work items.
- Creating, approving, commenting on, abandoning, or completing pull requests.
- Automatic CI repair, auto-merge, and Azure Pipelines log streaming.
- Service-hook/webhook ingestion; reads and refreshes use requests, local
invalidation after configuration mutations, and polling for recovery.
- Using the Azure PAT for agent-authored pushes, pull-request creation, or any
other write operation.
- Requiring Azure CLI or the Azure DevOps CLI extension. The existing optional
agentctl PR-create fallback remains separate until write support is added.
## Implementation Plan
See [the active implementation plan](../../plans/azure-devops-integration/plan.md).