mcpField
A
ado

ado

github.com/jimwellerDevelopment69 installs
Repository

About ado

A coding skill for operating Azure DevOps resources with the `az` CLI and REST API. It documents repository, pull request, service connection, agent pool, agent, and queue workflows, including remote URL parsing and environment-based defaults.

Key features

  1. 01Manage Azure DevOps projects and repositories
  2. 02Create and update pull requests
  3. 03Read and post PR comments
  4. 04Manage service connections
  5. 05Inspect agent pools and agents

Use cases

  1. 01List repositories in a project
  2. 02Review or complete a pull request
  3. 03Check service connection configuration
  4. 04Find online build agents

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/jimweller/clanker-skills --skill ado

After installing, type/adoin Claude Code to confirm it loaded.

Details

From the project README

STARTER_CHARACTER = 🔷

Azure DevOps Operations

Use the az CLI and ADO REST API to interact with Azure DevOps resources.

Prerequisites

Auth: $AZURE_DEVOPS_EXT_PAT (always set). No login needed.

Repo ops require ADO remote (dev.azure.com or visualstudio.com).

Default Org and Project

The shell environment provides default values via $ADO_DEFAULT_ORG and $ADO_DEFAULT_PROJECT. Use these as fallbacks when org or project cannot be parsed from the git remote, or when operating outside a git repo.

org="${org:-$ADO_DEFAULT_ORG}"
project="${project:-$ADO_DEFAULT_PROJECT}"

Pass them to az commands via --org "https://dev.azure.com/${org}" and --project "$project".

URL Parsing

Azure DevOps remotes come in three formats. Parse org, project, and repo from the remote URL to construct web URLs and pass parameters to az commands.

remote_url=$(git remote get-url origin 2>/dev/null)
Format 1: dev.azure.com HTTPS

Pattern: https://dev.azure.com/{org}/{project}/_git/{repo} or https://{user}@dev.azure.com/{org}/{project}/_git/{repo}

clean_url=$(echo "$remote_url" | sed 's|https://[^@]*@|https://|')
org=$(echo "$clean_url" | sed -n 's|https://dev\.azure\.com/\([^/]*\)/.*|\1|p')
project=$(echo "$clean_url" | sed -n 's|https://dev\.azure\.com/[^/]*/\([^/]*\)/_git/.*|\1|p')
repo=$(echo "$clean_url" | sed -n 's|.*/_git/\(.*\)|\1|p')
Format 2: visualstudio.com

Pattern: https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}

clean_url=$(echo "$remote_url" | sed 's|https://[^@]*@|https://|')
org=$(echo "$clean_url" | sed -n 's|^https://\([^.]*\)\..*|\1|p')
project=$(echo "$clean_url" | sed -n 's|.*/DefaultCollection/\([^/]*\)/_git/.*|\1|p')
repo=$(echo "$clean_url" | sed -n 's|.*/_git/\(.*\)|\1|p')
Format 3: SSH

Pattern: [email protected]:v3/{org}/{project}/{repo}

org=$(echo "$remote_url" | sed -n 's|git@ssh\.dev\.azure\.com:v3/\([^/]*\)/.*|\1|p')
project=$(echo "$remote_url" | sed -n 's|git@ssh\.dev\.azure\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
repo=$(echo "$remote_url" | sed -n 's|.*[^/]*/\([^/]*\)$|\1|p')
Constructing Web URLs

URL-encode spaces in project names (%20), then build:

base_url=https://dev.azure.com/{org}/{project}/_git/{repo}
pr_url={base_url}/pullrequest/{pr_id}

On macOS, open URLs with open "$url".


Browse

Open Repo

Parse the remote URL, construct the web URL, and open it.

open "https://dev.azure.com/${org}/${project}/_git/${repo}"
Open PR
open "https://dev.azure.com/${org}/${project}/_git/${repo}/pullrequest/${pr_id}"

Projects

List Projects
az devops project list --organization "https://dev.azure.com/${org}" --query "value[].{Name:name,ID:id,State:state}" -o table
Show Project Details
az devops project show --project "$project" --organization "https://dev.azure.com/${org}"
Get Project ID
az devops project show --project "$project" --organization "https://dev.azure.com/${org}" --query id -o tsv

Repositories

Create
az repos create \
  --name "<repo-name>" \
  --project "$project" \
  --org "https://dev.azure.com/${org}" \
  --output table \
  --query "{Name:name,Project:project.name,URL:webUrl}"
Delete

Step 1: Look up the repository ID.

repo_id=$(az repos show \
  --repository "<repo-name>" \
  --project "$project" \
  --query id --output tsv)

Step 2: Delete by ID.

az repos delete \
  --id "$repo_id" \
  --project "$project" \
  --yes
List
az repos list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,ID:id,DefaultBranch:defaultBranch}" -o table
Show
az repos show --repository <REPO_NAME> --organization "https://dev.azure.com/${org}" --project "$project"
Get Clone URL
az repos show --repository <REPO_NAME> --organization "https://dev.azure.com/${org}" --project "$project" --query remoteUrl -o tsv

Pull Requests (CLI)

All PR commands use JMESPath queries for structured output. The base_url variable is constructed from the parsed remote URL (see URL Parsing above).

Create
az repos pr create \
  "$@" \
  --output table \
  --query "{PR:pullRequestId,Status:status,URL:join(\`\`,[\ \`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"

Common flags: --title, --description, --source-branch, --target-branch, --reviewers, --auto-complete, --draft

List
az repos pr list \
  --output table \
  --query "[].{PR:pullRequestId,Title:title,Creator:createdBy.uniqueName,Created:creationDate,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"

Common flags: --status, --creator, --reviewer, --source-branch, --target-branch

Show
az repos pr show \
  --id <pr-id> \
  --output table \
  --query "{PR:pullRequestId,Title:title,Creator:createdBy.uniqueName,Created:creationDate,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"
Update
az repos pr update \
  --id <pr-id> \
  --output table \
  --query "{PR:pullRequestId,Status:status,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"

Common flags: --title, --description, --status, --auto-complete, --draft

Complete

Complete (merge) a pull request by setting status to completed with policy bypass.

az repos pr update \
  --id <pr-id> \
  --status completed \
  --bypass-policy true \
  --output table \
  --query "{PR:pullRequestId,Status:status,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"

Additional flags: --merge-commit-message

Completion is async. The response will still show active status. Follow up with az repos pr show --id <pr-id> to confirm status: completed and mergeStatus: succeeded.

Abandon
az repos pr update \
  --id <pr-id> \
  --status abandoned \
  --output table \
  --query "{PR:pullRequestId,Status:status,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"
Vote/Approve
az repos pr set-vote \
  --id <pr-id> \
  --vote approve

After voting, show the PR details:

az repos pr show \
  --id <pr-id> \
  --output table \
  --query "{PR:pullRequestId,Title:title,Creator:createdBy.uniqueName,Created:creationDate,URL:join(\`\`,[\`${base_url}\`,\`/pullrequest/\`,to_string(pullRequestId)])}"

Valid vote values: approve, approve-with-suggestions, wait-for-author, reject, reset


PR Comments and Threads (REST API)

Post Comment
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>/threads?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "comments": [{"parentCommentId": 0, "content": "Comment text here", "commentType": 1}],
    "status": 1
  }'
Get Comments/Threads
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>/threads?api-version=7.0" \
  | jq '.value[] | {id: .id, status: .status, comments: [.comments[] | {author: .author.displayName, content: .content}]}'
Search for Comment Containing Text
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>/threads?api-version=7.0" \
  | jq '.value[].comments[] | select(.content | contains("SEARCH_TEXT")) | {author: .author.displayName, content: .content}'

Service Connections

List
az devops service-endpoint list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,Type:type,ID:id,Scheme:authorization.scheme}" -o table
Show Details
az devops service-endpoint show --id <ENDPOINT_ID> --organization "https://dev.azure.com/${org}" --project "$project"
Get by Name
az devops service-endpoint list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<NAME>']" -o json
Delete
az devops service-endpoint delete --id <ENDPOINT_ID> --organization "https://dev.azure.com/${org}" --project "$project" --yes
Get WIF Details (REST API)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/serviceendpoint/endpoints?api-version=7.1" \
  | jq ".value[] | select(.name == \"<NAME>\") | {issuer: .authorization.parameters.workloadIdentityFederationIssuer, subject: .authorization.parameters.workloadIdentityFederationSubject}"
Authorize for All Pipelines (REST API)
SC_ID=$(az devops service-endpoint list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<NAME>'].id" -o tsv)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X PATCH \
  "https://dev.azure.com/${org}/${project}/_apis/pipelines/pipelinePermissions/endpoint/${SC_ID}?api-version=7.1-preview.1" \
  -H "Content-Type: application/json" \
  -d '{"allPipelines": {"authorized": true}}'

Agent Pools

Pools are organization-level and shared across projects.

List All
az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[].{Name:name,ID:id,PoolType:poolType,IsHosted:isHosted,Size:size}" -o table
List by Name Pattern
az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?contains(name, '<PATTERN>')].{Name:name,ID:id,PoolType:poolType,Size:size}" -o table
Get Pool ID by Name
az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv
Show Details
az pipelines pool show --organization "https://dev.azure.com/${org}" --pool-id <POOL_ID>
Create (REST API)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/_apis/distributedtask/pools?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{"name": "<POOL_NAME>", "poolType": "automation", "isHosted": false, "autoProvision": false}'
Delete (REST API)
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X DELETE \
  "https://dev.azure.com/${org}/_apis/distributedtask/pools/${POOL_ID}?api-version=7.1"

Agents

Agents run within pools.

List Agents in Pool
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
az pipelines agent list --organization "https://dev.azure.com/${org}" --pool-id $POOL_ID --query "[].{Name:name,ID:id,Status:status,Version:version}" -o table
List Online Agents
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
az pipelines agent list --organization "https://dev.azure.com/${org}" --pool-id $POOL_ID --query "[?status=='online'].{Name:name,ID:id,Version:version}" -o table
Show Agent Details
az pipelines agent show --organization "https://dev.azure.com/${org}" --pool-id <POOL_ID> --agent-id <AGENT_ID>
Count Online Agents
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
az pipelines agent list --organization "https://dev.azure.com/${org}" --pool-id $POOL_ID --query "[?status=='online']" -o tsv | wc -l

Queues

Queues are project-level references to organization-level pools. Pipeline authorization works on queues, not pools.

List All
az pipelines queue list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,QueueID:id,PoolID:pool.id}" -o table
List by Name Pattern
az pipelines queue list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?contains(name, '<PATTERN>')].{Name:name,QueueID:id,PoolID:pool.id}" -o table
Get Queue ID for Pool
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
az pipelines queue list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?pool.id==\`${POOL_ID}\`].id" -o tsv
Create Queue for Pool (REST API)
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/distributedtask/queues?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{"name": "<QUEUE_NAME>", "pool": {"id": '${POOL_ID}'}}'
Authorize Queue for All Pipelines (REST API)
QUEUE_ID=$(az pipelines queue list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<QUEUE_NAME>'].id" -o tsv)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X PATCH \
  "https://dev.azure.com/${org}/${project}/_apis/pipelines/pipelinePermissions/queue/${QUEUE_ID}?api-version=7.1-preview.1" \
  -H "Content-Type: application/json" \
  -d '{"allPipelines": {"authorized": true}}'
Authorize Queue for Specific Pipeline (REST API)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X PATCH \
  "https://dev.azure.com/${org}/${project}/_apis/pipelines/pipelinePermissions/queue/<QUEUE_ID>?api-version=7.1-preview.1" \
  -H "Content-Type: application/json" \
  -d '{"pipelines": [{"id": <PIPELINE_ID>, "authorized": true}]}'

Pipelines

List All
az pipelines list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,ID:id,Folder:folder,QueueStatus:queueStatus}" -o table
List by Name Pattern
az pipelines list --organization "https://dev.azure.com/${org}" --project "$project" --name "<PATTERN>*" --query "[].{Name:name,ID:id}" -o table
Show Details
az pipelines show --organization "https://dev.azure.com/${org}" --project "$project" --name <PIPELINE_NAME>
Get Pipeline ID by Name
az pipelines list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<PIPELINE_NAME>'].id" -o tsv
Create
az pipelines create \
  --organization "https://dev.azure.com/${org}" \
  --project "$project" \
  --name <PIPELINE_NAME> \
  --repository <REPO_NAME> \
  --repository-type tfsgit \
  --branch main \
  --yml-path azure-pipelines.yml \
  --skip-first-run
Delete
PIPELINE_ID=$(az pipelines list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<PIPELINE_NAME>'].id" -o tsv)
az pipelines delete --id $PIPELINE_ID --organization "https://dev.azure.com/${org}" --project "$project" --yes

Pipeline Runs (Builds)

Run Pipeline
az pipelines run --organization "https://dev.azure.com/${org}" --project "$project" --name <PIPELINE_NAME>
Run and Get Run ID
RUN_ID=$(az pipelines run --organization "https://dev.azure.com/${org}" --project "$project" --name <PIPELINE_NAME> --query id -o tsv)
echo "Run ID: $RUN_ID"
Run with Variables
az pipelines run --organization "https://dev.azure.com/${org}" --project "$project" --name <PIPELINE_NAME> --variables "VAR1=value1" "VAR2=value2"
List Recent Runs
az pipelines runs list --organization "https://dev.azure.com/${org}" --project "$project" --top 10 --query "[].{ID:id,Pipeline:definition.name,Status:status,Result:result,StartTime:startTime}" -o table
List Runs for Specific Pipeline
PIPELINE_ID=$(az pipelines list --organization "https://dev.azure.com/${org}" --project "$project" --query "[?name=='<PIPELINE_NAME>'].id" -o tsv)
az pipelines runs list --organization "https://dev.azure.com/${org}" --project "$project" --pipeline-ids $PIPELINE_ID --query "[].{ID:id,Status:status,Result:result}" -o table
Show Run Status
az pipelines runs show --organization "https://dev.azure.com/${org}" --project "$project" --id <RUN_ID> --query "{Status:status,Result:result}" -o json
Watch Run Until Complete
RUN_ID=<RUN_ID>
while true; do
  STATUS=$(az pipelines runs show --organization "https://dev.azure.com/${org}" --project "$project" --id $RUN_ID --query status -o tsv)
  RESULT=$(az pipelines runs show --organization "https://dev.azure.com/${org}" --project "$project" --id $RUN_ID --query result -o tsv)
  echo "Status: $STATUS, Result: ${RESULT:-pending}"
  if [ "$STATUS" == "completed" ]; then break; fi
  sleep 10
done

Build Logs

Get Build Timeline (Stages/Jobs)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/build/builds/<RUN_ID>/timeline?api-version=7.1" \
  | jq '.records[] | {name: .name, type: .type, state: .state, result: .result}'
List Build Logs
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/build/builds/<RUN_ID>/logs?api-version=7.1" \
  | jq '.value[] | {id: .id, type: .type, lineCount: .lineCount}'
Download Specific Log
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/build/builds/<RUN_ID>/logs/<LOG_ID>?api-version=7.1"
Get Failed Step Logs
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/build/builds/<RUN_ID>/timeline?api-version=7.1" \
  | jq '.records[] | select(.result == "failed") | {name: .name, log: .log.url}'

Git Operations (REST API)

For programmatic Git workflows (branches, commits, pushes) without a local clone.

Get Repository by Name
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>?api-version=7.0" \
  | jq '{id: .id, name: .name, defaultBranch: .defaultBranch}'
Get Branch Refs
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/refs?filter=heads/main&api-version=7.0" \
  | jq '.value[] | {name: .name, objectId: .objectId}'
Get Main Branch Commit SHA
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/refs?filter=heads/main&api-version=7.0" \
  | jq -r '.value[0].objectId'
Create Branch from Main
MAIN_SHA=$(curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/refs?filter=heads/main&api-version=7.0" \
  | jq -r '.value[0].objectId')

curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/refs?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '[{"name": "refs/heads/<BRANCH_NAME>", "oldObjectId": "0000000000000000000000000000000000000000", "newObjectId": "'${MAIN_SHA}'"}]'
Delete Branch
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/refs?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '[{"name": "refs/heads/<BRANCH_NAME>", "oldObjectId": "<LAST_COMMIT_SHA>", "newObjectId": "0000000000000000000000000000000000000000"}]'
Push a Commit (Add/Edit Files)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pushes?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "refUpdates": [{"name": "refs/heads/<BRANCH_NAME>", "oldObjectId": "<BASE_COMMIT_SHA>"}],
    "commits": [{
      "comment": "Commit message here",
      "changes": [
        {
          "changeType": "edit",
          "item": {"path": "/path/to/file"},
          "newContent": {"content": "file content here", "contentType": "rawtext"}
        }
      ]
    }]
  }'
Push Initial Commit (New Repository)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pushes?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "refUpdates": [{"name": "refs/heads/main", "oldObjectId": "0000000000000000000000000000000000000000"}],
    "commits": [{
      "comment": "Initial commit",
      "changes": [
        {
          "changeType": "add",
          "item": {"path": "/README.md"},
          "newContent": {"content": "# Repo Title", "contentType": "rawtext"}
        }
      ]
    }]
  }'
Push with Base64-Encoded Content
CONTENT_B64=$(echo -n "file contents here" | base64)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pushes?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "refUpdates": [{"name": "refs/heads/<BRANCH>", "oldObjectId": "<SHA>"}],
    "commits": [{
      "comment": "Add file",
      "changes": [{
        "changeType": "add",
        "item": {"path": "/file.txt"},
        "newContent": {"content": "'${CONTENT_B64}'", "contentType": "base64encoded"}
      }]
    }]
  }'

Pull Requests (REST API)

Alternative to az repos pr commands when more control is needed.

Create
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullrequests?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceRefName": "refs/heads/<SOURCE_BRANCH>",
    "targetRefName": "refs/heads/main",
    "title": "PR Title",
    "description": "PR description"
  }' | jq '{pullRequestId: .pullRequestId, status: .status}'
Get Details
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>?api-version=7.0" \
  | jq '{id: .pullRequestId, title: .title, status: .status, sourceRefName: .sourceRefName}'
List
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullrequests?api-version=7.0" \
  | jq '.value[] | {id: .pullRequestId, title: .title, status: .status}'
Abandon
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X PATCH \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{"status": "abandoned"}'
Complete (Merge)
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X PATCH \
  "https://dev.azure.com/${org}/${project}/_apis/git/repositories/<REPO_NAME>/pullRequests/<PR_ID>?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "completed",
    "lastMergeSourceCommit": {"commitId": "<LAST_COMMIT_SHA>"},
    "completionOptions": {"deleteSourceBranch": true, "mergeStrategy": "squash"}
  }'

Code Search (REST API)

Search code across repositories. Uses the almsearch.dev.azure.com host, not dev.azure.com.

Search in Project
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{"searchText":"<TEXT>","$top":25}' \
  | jq '{count: .count, results: [.results[] | {repo: .repository.name, path: .path}]}'
Search Across Org with Filters
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://almsearch.dev.azure.com/${org}/_apis/search/codesearchresults?api-version=7.1" \
  -H "Content-Type: application/json" \
  -d '{
    "searchText":"<TEXT>",
    "filters":{"Project":["<PROJECT>"],"Repository":["<REPO>"],"Branch":["main"],"Path":["/src"]},
    "$top":25
  }' \
  | jq '.results[] | {project: .project.name, repo: .repository.name, path: .path}'

Available filter keys: Project, Repository, Branch, Path, CodeElement (e.g. ["def","class"]).

Search Operators

The searchText field accepts ADO search syntax:

  • ext:go quiver-bolt -- file extension filter
  • file:Makefile quiver-bolt -- filename filter
  • def:MyFunction -- function/method definitions
  • "exact phrase" -- exact match
  • foo AND bar, foo OR bar, foo NOT bar -- boolean

Webhooks (REST API)

List Service Hooks
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" \
  "https://dev.azure.com/${org}/_apis/hooks/subscriptions?api-version=7.0" \
  | jq '.value[] | {id: .id, eventType: .eventType, consumerActionId: .consumerActionId, publisherInputs: .publisherInputs}'
Create Webhook for PR Events
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X POST \
  "https://dev.azure.com/${org}/_apis/hooks/subscriptions?api-version=7.0" \
  -H "Content-Type: application/json" \
  -d '{
    "publisherId": "tfs",
    "eventType": "git.pullrequest.created",
    "resourceVersion": "1.0",
    "consumerId": "webHooks",
    "consumerActionId": "httpRequest",
    "publisherInputs": {
      "projectId": "<PROJECT_ID>",
      "repository": "<REPO_ID>"
    },
    "consumerInputs": {
      "url": "<WEBHOOK_URL>"
    }
  }'
Delete Webhook
curl -s -u ":${AZURE_DEVOPS_EXT_PAT}" -X DELETE \
  "https://dev.azure.com/${org}/_apis/hooks/subscriptions/<SUBSCRIPTION_ID>?api-version=7.0"

Diagnostics

Check ADO Infrastructure Health
echo "=== Service Connections ==="
az devops service-endpoint list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,Type:type}" -o table

echo ""
echo "=== Agent Pools ==="
az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[].{Name:name,Size:size,IsHosted:isHosted}" -o table

echo ""
echo "=== Queues ==="
az pipelines queue list --organization "https://dev.azure.com/${org}" --project "$project" --query "[].{Name:name,QueueID:id}" -o table
Check Agents in a Pool
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
if [ -n "$POOL_ID" ]; then
  az pipelines agent list --organization "https://dev.azure.com/${org}" --pool-id $POOL_ID --query "[].{Name:name,Status:status}" -o table
fi
Wait for Agents to Come Online
POOL_ID=$(az pipelines pool list --organization "https://dev.azure.com/${org}" --query "[?name=='<POOL_NAME>'].id" -o tsv)
MAX_WAIT=300
ELAPSED=0
while [ $ELAPSED -lt $MAX_WAIT ]; do
  ONLINE=$(az pipelines agent list --organization "https://dev.azure.com/${org}" --pool-id $POOL_ID --query "[?status=='online']" -o tsv | wc -l)
  if [ "$ONLINE" -gt 0 ]; then
    echo "Found $ONLINE online agent(s)"
    break
  fi
  echo "Waiting for agents... ($ELAPSED/${MAX_WAIT}s)"
  sleep 10
  ELAPSED=$((ELAPSED + 10))
done

Key Concepts

Pool vs Queue
  • Pools are organization-level (shared across projects)
  • Queues are project-level references to pools
  • Pipeline authorization works on queues, not pools
Workload Identity Federation

Service connections use WIF (OIDC). No secrets stored in ADO. Federated credentials are managed in App Registration.

REST API vs CLI

Some operations require REST API (no CLI equivalent):

  • Creating pools with specific settings
  • Authorizing queues for pipelines
  • Querying WIF subjects from service connections
  • Git push operations (branches, commits)
  • PR comment threads
  • Webhooks

Comments

Markdown supported