> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Azure DevOps

Loading version...

Port's Azure DevOps integration allows you to model Azure DevOps resources in your software catalog and ingest data into them.

Supported deployments models

This integration supports both Azure DevOps Services (cloud) and Azure DevOps Server (on-premises).

Note: When using the on-premises model, releases and work items are not supported due to API version differences. All other supported resources work the same as the cloud version.

Service principal authentication (via Microsoft Entra ID) is available for Azure DevOps Services (cloud) and works for both single-account and multi-account installations. Azure DevOps Server (on-premises) must continue to use a personal access token.

Prerequisites

  • An Azure DevOps account with admin privileges.
  • If you choose the self-hosted installation method, you will need a Kubernetes cluster on which to install the integration.
  • Your Port user role is set to Admin.
Backwards compatibility

Existing installations that authenticate with a personal access token and a single organization URL continue to work without any changes. When accountMode is not set, the integration defaults to single-account mode.

Authentication methods

The integration supports two authentication methods. Choose the one that matches your deployment:

  • Personal access token (PAT): Authenticates against a single Azure DevOps organization (or a single on-premises collection). This is required for Azure DevOps Server (on-premises).
  • Service principal (recommended): Uses a Microsoft Entra ID application to authenticate with your Azure DevOps organization. This is available for Azure DevOps Services (cloud).
Personal access tokens are being retired

Microsoft is retiring global personal access tokens in Azure DevOps on December 1, 2026, after which existing global PATs will stop working. For Azure DevOps Services (cloud), we recommend migrating to service principal authentication, which uses short-lived, Microsoft Entra-backed credentials. See Microsoft's announcement: Retirement of global personal access tokens in Azure DevOps.

Version requirement

Service principal authentication requires Azure DevOps integration version 0.9.0 or later.

We will create a Microsoft Entra ID application, grant it access to each Azure DevOps organization you want to sync, and provide the resulting credentials to the integration. The same flow applies whether you sync a single organization or multiple organizations from one installation.

  1. Register an application in Microsoft Entra ID. Follow Register an application with the Microsoft identity platform. After registration, record the Application (client) ID and the Directory (tenant) ID from the app's Overview page.

  2. Create a client secret. On the app registration, go to Certificates & secrets and create a new client secret. Copy the secret value immediately (it will not be shown again).

  3. Add the service principal to each Azure DevOps organization you want to sync. Follow Use service principals and managed identities in Azure DevOps. For every target organization:

    • Open the organization at https://dev.azure.com/{organization}.
    • Navigate to Organization settings > Users > Add users.
    • Search for the Enterprise Application's display name (the same name as the app registration you created).
    • Assign an access level of Basic or higher. Stakeholder is not sufficient for repository access.
  4. Grant the service principal permission on each project or repository you want to sync. Use the same Azure DevOps permission model you would apply to a user (for example, Project Settings > Permissions or Repository Settings > Security).

  5. Collect the organization URLs you want to sync. Each URL must follow the format https://dev.azure.com/{organization}. For example:

    https://dev.azure.com/my-org
    https://dev.azure.com/another-org

    You will pass these as a list to the integration.

Required permissions (click to expand)
  • Azure DevOps does not use Microsoft Entra ID application permissions. All access control is enforced through the Azure DevOps permission system, so the app registration itself does not need any API permissions configured. See the Microsoft docs for the full explanation.
  • Permissions must be granted separately inside each Azure DevOps organization you sync, using that organization's permission model.
  • Each organization counts as a separate license for the service principal (no multi-organization licensing discount).
  • The service principal must belong to the same Microsoft Entra tenant that each target Azure DevOps organization is connected to. Cross-tenant access requires Microsoft's documented key-vault and certificate workaround.
Sync time grows with organizations

When syncing multiple organizations, Port processes a few at a time in parallel rather than all at once. Azure DevOps also enforces its own API rate limits separately for each organization. As a result, the more organizations you sync, the longer a full resync takes.

Once complete, you will have:

  • A list of organization URLs (for example, https://dev.azure.com/org-a and https://dev.azure.com/org-b).
  • A tenant ID.
  • A client ID.
  • A client secret value.

Provide these four values to the integration using the installation method you chose below.

Setup

Choose your preferred installation method below. Not sure which to pick? See the installation methods overview.

Configuration

Port integrations use a YAML mapping block to ingest data from the third-party api into Port.

The mapping makes use of the JQ JSON processor to select, modify, concatenate, transform and perform other operations on existing fields and values from the integration API.

Default mapping configuration

This is the default mapping configuration for this integration:

Default mapping configuration (click to expand)
deleteDependentEntities: true
createMissingRelatedEntities: true
enableMergeEntity: true
resources:
- kind: project
selector:
query: 'true'
defaultTeam: 'false'
port:
entity:
mappings:
identifier: .id | gsub(" "; "")
blueprint: '"azureDevopsProject"'
title: .name
properties:
state: .state
revision: .revision
visibility: .visibility
defaultTeam: .defaultTeam.name
link: .url | gsub("_apis/projects/"; "")
- kind: repository
selector:
query: 'true'
includedFiles:
- README.md
port:
entity:
mappings:
identifier: .id
title: .name
blueprint: '"azureDevopsRepository"'
properties:
url: .remoteUrl
readme: .__includedFiles["README.md"]
id: .id
last_activity: .project.lastUpdateTime
relations:
project: .project.id | gsub(" "; "")
- kind: repository-policy
selector:
query: .type.displayName=="Minimum number of reviewers"
port:
entity:
mappings:
identifier: .__repository.id
blueprint: '"azureDevopsRepository"'
properties:
minimumApproverCount: .settings.minimumApproverCount
- kind: repository-policy
selector:
query: .type.displayName=="Work item linking"
port:
entity:
mappings:
identifier: .__repository.id
blueprint: '"azureDevopsRepository"'
properties:
workItemLinking: .isEnabled and .isBlocking
- kind: user
selector:
query: 'true'
port:
entity:
mappings:
identifier: .id
title: .user.displayName
blueprint: '"azureDevopsMember"'
properties:
url: .user.url
email: .user.mailAddress
- kind: team
selector:
query: 'true'
includeMembers: true
port:
entity:
mappings:
identifier: .id
title: .name
blueprint: '"azureDevopsTeam"'
properties:
url: .url
description: .description
relations:
project: .projectId | gsub(" "; "")
members: .__members | map(.identity.id)
- kind: pull-request
selector:
query: 'true'
port:
entity:
mappings:
identifier: .repository.id + "/" + (.pullRequestId | tostring)
blueprint: '"azureDevopsPullRequest"'
properties:
status: .status
createdAt: .creationDate
leadTimeHours: (.creationDate as $createdAt | .status as $status | .closedDate as $closedAt | ($createdAt | sub("\\..*Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as $createdTimestamp | ($closedAt | if . == null then null else sub("\\..*Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as $closedTimestamp | if $status == "completed" and $closedTimestamp != null then (((($closedTimestamp - $createdTimestamp) / 3600) * 100 | floor) / 100) else null end)
relations:
repository: .repository.id
service:
combinator: '"and"'
rules:
- operator: '"="'
property: '"ado_repository_id"'
value: .repository.id
creator:
combinator: '"and"'
rules:
- operator: '"="'
property: '"$identifier"'
value: .createdBy.uniqueName
reviewers:
combinator: '"and"'
rules:
- operator: '"in"'
property: '"$identifier"'
value: '[.reviewers[].uniqueName]'
azure_devops_reviewers: '[.reviewers[].id]'
azure_devops_creator: .createdBy.id
- kind: build
selector:
query: "true"
port:
entity:
mappings:
identifier: .__project.id + "/" + (.repository.id | tostring) + "/" + (.id | tostring) | gsub(" "; "")
title: .buildNumber
blueprint: '"azureDevopsBuild"'
properties:
status: .status
result: .result
queueTime: .queueTime
startTime: .startTime
finishTime: .finishTime
definitionName: .definition.name
requestedFor: .requestedFor.displayName
link: ._links.web.href
relations:
project: .__project.id | gsub(" "; "")
- kind: pipeline-stage
selector:
query: 'true'
port:
entity:
mappings:
identifier: .__project.id + "/" + (.__build.repository.id | tostring) + "/" + (.__build.id | tostring) + "/" + (.id | tostring) | gsub(" "; "")
title: .name
blueprint: '"azureDevopsPipelineStage"'
properties:
state: .state
result: .result
startTime: .startTime
finishTime: .finishTime
stageType: .type
relations:
project: .__project.id | gsub(" "; "")
build: (.__project.id + "/" + (.__build.repository.id | tostring) + "/" + (.__build.id | tostring)) | gsub(" "; "")
- kind: pipeline-run
selector:
query: 'true'
port:
entity:
mappings:
identifier: .__project.id + "/" + (.__pipeline.id | tostring) + "/" + (.id | tostring) | gsub(" "; "")
blueprint: '"azureDevopsPipelineRun"'
properties:
state: .state
result: .result
createdDate: .createdDate
finishedDate: .finishedDate
pipelineName: .pipeline.name
relations:
project: .__project.id | gsub(" "; "")
- kind: environment
selector:
query: 'true'
port:
entity:
mappings:
identifier: .project.id + "/" + (.id | tostring) | gsub(" "; "")
title: .name | tostring
blueprint: '"azureDevopsEnvironment"'
properties:
description: .description
createdOn: .createdOn
lastModifiedOn: .lastModifiedOn
relations:
project: .project.id
- kind: release-deployment
selector:
query: 'true'
includeRelease: true
port:
entity:
mappings:
identifier: .releaseDefinition.projectReference.id + "/" + (.release.id | tostring) + "/" + (.id | tostring) | gsub(" "; "")
title: .release.name + "-" + (.id | tostring) | gsub(" "; "")
blueprint: '"azureDevopsReleaseDeployment"'
properties:
status: .deploymentStatus
url: .url
reason: .reason
startedOn: .startedOn
completedOn: .completedOn
requestedBy: .requestedBy.displayName
operationStatus: .operationStatus
environment: .releaseEnvironment.name
relations:
project: .releaseDefinition.projectReference.id | gsub(" "; "")
release: .releaseDefinition.projectReference.id + "/" + (.release.id | tostring) | gsub(" "; "")
- kind: pipeline-deployment
selector:
query: 'true'
port:
entity:
mappings:
identifier: .__project.id + "/" + (.environmentId | tostring) + "/" + (.id | tostring) | gsub(" "; "")
title: .requestIdentifier | tostring
blueprint: '"azureDevopsPipelineDeployment"'
properties:
planType: .planType
stageName: .stageName
jobName: .jobName
result: .result
startTime: .startTime
finishTime: .finishTime
relations:
project: .__project.id | gsub(" "; "")
environment: .__project.id + "/" + (.environmentId | tostring) | gsub(" "; "")

Multi-organization mapping

When the integration runs in multi-organization mode, every ingested entity is enriched with two extra fields that you can reference in JQ:

  • __organizationUrl: the full URL of the Azure DevOps organization the entity came from (for example, https://dev.azure.com/org-a).
  • __organizationName: the short organization name extracted from that URL (for example, org-a).

Use these fields to disambiguate entities that share an ID across organizations, or to surface the source organization on each entity.

Example multi-organization mapping (Click to expand)
resources:
- kind: project
selector:
query: 'true'
port:
entity:
mappings:
identifier: .__organizationName + "/" + (.id | gsub(" "; ""))
blueprint: '"azureDevopsProject"'
title: .__organizationName + " / " + .name
properties:
organization: .__organizationName
organizationUrl: .__organizationUrl
state: .state
visibility: .visibility
- kind: repository
selector:
query: 'true'
port:
entity:
mappings:
identifier: .__organizationName + "/" + .id
title: .__organizationName + " / " + .name
blueprint: '"azureDevopsRepository"'
properties:
url: .remoteUrl
organization: .__organizationName
organizationUrl: .__organizationUrl
relations:
project: .__organizationName + "/" + (.project.id | gsub(" "; ""))

The default mapping above continues to work for single-organization installations without any changes. Add the snippet above only if you use the service principal authentication method and sync multiple organizations.

Mapping & selectors per resource

Use the explorer below to view sample payloads and the resulting Port entities for each resource type. For additional resources and advanced configurations, see the examples page.

Monitoring and sync status

To learn more about how to monitor and check the sync status of your integration, see the relevant documentation.

Examples

Refer to the examples page for practical configurations and their corresponding blueprint definitions.

Relevant Guides

For relevant guides and examples, see the guides section.

GitOps

Port's Azure DevOps integration also provides GitOps capabilities, refer to the GitOps page to learn more.

Advanced

Refer to the advanced page for advanced use cases and examples.