Azure DevOps
Port's Azure DevOps integration allows you to model Azure DevOps resources in your software catalog and ingest data into them.
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
- Single Account
- Multiple Accounts
- 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.
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).
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.
- Service principal (recommended)
- Personal access token
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.
-
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.
-
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).
-
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.
- Open the organization at
-
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).
-
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-orghttps://dev.azure.com/another-orgYou 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.
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-aandhttps://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.
The integration requires a personal access token to authenticate with your Azure DevOps account. Follow Microsoft's PAT guide to create one.
The token should either have admin permissions, or read permissions for each of the supported resources you want to ingest into Port.
Personal access tokens are scoped to a single organization (or a single on-premises collection). To sync multiple organizations from one installation, switch to the Multiple Accounts tab above.
- An Azure DevOps account with admin privileges in each organization you want to sync.
- 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. - All target Azure DevOps organizations must be backed by the same Microsoft Entra tenant as the service principal.
Authentication
Multi-account installations require service principal authentication. Personal access tokens are scoped to a single organization, so they cannot be used in multi-account mode.
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.
-
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.
-
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).
-
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.
- Open the organization at
-
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).
-
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-orghttps://dev.azure.com/another-orgYou 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.
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-aandhttps://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.
- Single Account
- Multiple Accounts
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.