Integrations

Azure DevOps CLI: Command-Line Productivity

A comprehensive guide to the Azure DevOps CLI extension for Azure CLI, covering installation, configuration, work item management, pipeline operations, repository commands, pull request workflows, and automation scripting for maximum command-line productivity.

Azure DevOps CLI: Command-Line Productivity

Overview

The Azure DevOps CLI is an extension to the Azure CLI that brings the full power of Azure DevOps to your terminal. Instead of clicking through the web portal to create work items, queue builds, manage pull requests, or check pipeline status, you run a single command. For engineers who live in the terminal, this is a massive productivity boost. I switched to the CLI for my daily Azure DevOps interactions two years ago and I cannot imagine going back to the web portal for routine tasks. The CLI is also the foundation for shell scripts that automate repetitive DevOps workflows.

Prerequisites

  • Azure CLI installed (az command available)
  • Azure DevOps organization and project
  • Personal Access Token or Azure AD login
  • Bash, PowerShell, or any shell environment
  • Familiarity with basic Azure DevOps concepts (work items, repos, pipelines)

Installation and Configuration

Install the Extension

# Install the Azure DevOps extension
az extension add --name azure-devops

# Verify installation
az devops --help

Authentication

# Option 1: Log in with Azure AD (interactive)
az login

# Option 2: Log in with a PAT (non-interactive, best for scripts)
export AZURE_DEVOPS_EXT_PAT="your-personal-access-token"

# Option 3: Pipe PAT via stdin
echo "your-pat" | az devops login --organization https://dev.azure.com/your-org

Set Defaults

Set your organization and project as defaults so you do not have to pass them with every command:

# Set default organization and project
az devops configure --defaults organization=https://dev.azure.com/your-org project=your-project

# Verify defaults
az devops configure --list

With defaults set, commands like az boards work-item show --id 1234 work without specifying --org and --project every time.

Work Item Management

Creating Work Items

# Create a user story
az boards work-item create \
  --type "User Story" \
  --title "Implement user profile page" \
  --assigned-to "[email protected]" \
  --area "your-project\\Frontend" \
  --iteration "your-project\\Sprint 5" \
  --description "Build the user profile page with avatar upload, bio editing, and notification preferences."

# Create a bug with priority
az boards work-item create \
  --type Bug \
  --title "Login fails with special characters in password" \
  --assigned-to "[email protected]" \
  --fields "Microsoft.VSTS.Common.Priority=1" "Microsoft.VSTS.Common.Severity=2 - High"

# Create a task under a parent story
az boards work-item create \
  --type Task \
  --title "Design profile page wireframes" \
  --fields "System.Parent=1234"

Querying Work Items

# Show a specific work item
az boards work-item show --id 1234

# Show with specific fields only
az boards work-item show --id 1234 --fields "System.Title,System.State,System.AssignedTo"

# List work items from a saved query
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] = 'Active' ORDER BY [Microsoft.VSTS.Common.Priority]"

# Output as table for readability
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.WorkItemType] = 'Bug' AND [System.State] <> 'Closed'" --output table

Updating Work Items

# Change state
az boards work-item update --id 1234 --state "Active"

# Assign to someone
az boards work-item update --id 1234 --assigned-to "[email protected]"

# Update multiple fields
az boards work-item update --id 1234 \
  --state "Resolved" \
  --fields "Microsoft.VSTS.Common.ResolvedReason=Fixed"

# Add a comment
az boards work-item update --id 1234 --discussion "Fixed in PR #567. Deployed to staging."

Bulk Operations

# Close all resolved bugs assigned to me
az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] = 'Resolved' AND [System.WorkItemType] = 'Bug'" --output json | \
  jq -r '.[] | .id' | \
  while read id; do
    az boards work-item update --id "$id" --state "Closed"
    echo "Closed bug #$id"
  done

# Create multiple tasks from a file
cat tasks.txt | while read title; do
  az boards work-item create --type Task --title "$title" --fields "System.Parent=1234"
done

Pipeline Operations

Listing and Running Pipelines

# List all pipeline definitions
az pipelines list --output table

# Show a specific pipeline
az pipelines show --id 42

# Run a pipeline
az pipelines run --id 42 --branch main

# Run with parameters
az pipelines run --id 42 --branch main --parameters "environment=staging" "skipTests=false"

# Run and open the result in browser
az pipelines run --id 42 --branch main --open

Monitoring Builds

# List recent builds
az pipelines build list --top 10 --output table

# List failed builds
az pipelines build list --result failed --top 5 --output table

# Show build details
az pipelines build show --id 9876

# Show build logs
az pipelines runs show --pipeline-id 42 --run-id 9876

# Get build timeline (stages/jobs/steps)
az pipelines build show --id 9876 --output json | jq '.timeline'

Pipeline Variables

# List pipeline variables
az pipelines variable list --pipeline-id 42 --output table

# Create a new variable
az pipelines variable create --pipeline-id 42 --name "DEPLOY_TARGET" --value "staging"

# Update a variable
az pipelines variable update --pipeline-id 42 --name "DEPLOY_TARGET" --new-value "production"

# Create a secret variable
az pipelines variable create --pipeline-id 42 --name "API_KEY" --value "secret-value" --is-secret true

Variable Groups

# List variable groups
az pipelines variable-group list --output table

# Show a specific group
az pipelines variable-group show --id 5

# Create a variable group
az pipelines variable-group create \
  --name "production-config" \
  --variables DB_HOST=prod-db.example.com DB_PORT=5432

# Add a variable to a group
az pipelines variable-group variable create \
  --group-id 5 \
  --name "NEW_VAR" \
  --value "some-value"

Repository Operations

Working with Repos

# List repos in the project
az repos list --output table

# Show repo details
az repos show --repository my-app

# Create a new repo
az repos create --name "new-service"

# Clone a repo (outputs the clone URL)
az repos show --repository my-app --query sshUrl --output tsv

Branch Management

# List branches
az repos ref list --repository my-app --filter heads/ --output table

# Create a branch from main
az repos ref create \
  --repository my-app \
  --name "refs/heads/feature/new-api" \
  --object-id $(az repos ref list --repository my-app --filter heads/main --query '[0].objectId' --output tsv)

# Delete a branch
az repos ref delete \
  --repository my-app \
  --name "refs/heads/feature/old-branch" \
  --object-id $(az repos ref list --repository my-app --filter heads/feature/old-branch --query '[0].objectId' --output tsv)

Branch Policies

# List branch policies
az repos policy list --repository-id $(az repos show -r my-app --query id -o tsv) --output table

# Create a minimum reviewers policy
az repos policy approver-count create \
  --repository-id $(az repos show -r my-app --query id -o tsv) \
  --branch main \
  --minimum-approver-count 2 \
  --creator-vote-counts false \
  --allow-downvotes false \
  --reset-on-source-push true \
  --blocking true \
  --enabled true

# Create a build validation policy
az repos policy build create \
  --repository-id $(az repos show -r my-app --query id -o tsv) \
  --branch main \
  --build-definition-id 42 \
  --display-name "CI Build" \
  --blocking true \
  --enabled true \
  --queue-on-source-update-only true \
  --valid-duration 720

Pull Request Workflows

Creating and Managing PRs

# Create a pull request
az repos pr create \
  --repository my-app \
  --source-branch feature/new-api \
  --target-branch main \
  --title "Add REST API for user profiles" \
  --description "Implements GET, POST, PUT, DELETE for /api/users/{id}/profile. Closes AB#1234."

# Create and auto-complete when policies pass
az repos pr create \
  --repository my-app \
  --source-branch feature/new-api \
  --target-branch main \
  --title "Add REST API for user profiles" \
  --auto-complete true \
  --delete-source-branch true \
  --squash true

# List open PRs
az repos pr list --status active --output table

# List PRs assigned to me for review
az repos pr list --reviewer-id $(az devops user show --user "me" --query id --output tsv) --output table

# Show PR details
az repos pr show --id 567

# Add a reviewer
az repos pr reviewer add --id 567 --reviewers "[email protected]"

# Set my vote (approve)
az repos pr set-vote --id 567 --vote approve

# Complete a PR
az repos pr update --id 567 --status completed --squash true --delete-source-branch true

PR Comments

# List PR threads (comments)
az repos pr thread list --id 567 --output table

# Add a comment to a PR
az repos pr thread create --id 567 --description "Looks good overall. One suggestion on the error handling in the auth middleware."

Automation Scripts

Daily Standup Report

Generate a standup report showing what you worked on yesterday and what is planned today:

#!/bin/bash
# standup-report.sh

ORG="https://dev.azure.com/your-org"
PROJECT="your-project"

echo "=== STANDUP REPORT ==="
echo "Date: $(date +%Y-%m-%d)"
echo ""

echo "--- Completed Yesterday ---"
az boards query \
  --org "$ORG" -p "$PROJECT" \
  --wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.ChangedDate] >= @today - 1 AND [System.State] IN ('Closed', 'Resolved', 'Done')" \
  --output table 2>/dev/null

echo ""
echo "--- In Progress ---"
az boards query \
  --org "$ORG" -p "$PROJECT" \
  --wiql "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] = 'Active'" \
  --output table 2>/dev/null

echo ""
echo "--- PRs Awaiting Review ---"
az repos pr list \
  --org "$ORG" -p "$PROJECT" \
  --status active \
  --output table 2>/dev/null

echo ""
echo "--- Recent Build Failures ---"
az pipelines build list \
  --org "$ORG" -p "$PROJECT" \
  --result failed \
  --top 5 \
  --output table 2>/dev/null

Sprint Cleanup Script

Close stale items and generate a sprint summary:

#!/bin/bash
# sprint-cleanup.sh

echo "=== Sprint Cleanup ==="

# Find resolved items that should be closed
RESOLVED=$(az boards query \
  --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Resolved' AND [System.ChangedDate] < @today - 3 AND [System.AssignedTo] = @me" \
  --output json 2>/dev/null | jq -r '.[].id')

if [ -n "$RESOLVED" ]; then
  echo "Closing stale resolved items..."
  for id in $RESOLVED; do
    az boards work-item update --id "$id" --state "Closed" --discussion "Auto-closed: resolved for 3+ days" > /dev/null
    echo "  Closed #$id"
  done
else
  echo "No stale resolved items found."
fi

# Sprint summary
echo ""
echo "--- Sprint Summary ---"
az boards query \
  --wiql "SELECT [System.Id], [System.Title], [System.State], [System.WorkItemType] FROM WorkItems WHERE [System.IterationPath] = @CurrentIteration AND [System.AssignedTo] = @me ORDER BY [System.State]" \
  --output table 2>/dev/null

Complete Working Example: Project Setup Automation

This script automates the setup of a new microservice project — creates the repo, sets up branch policies, creates the pipeline, initializes work items, and configures the team board:

// setup-project.js
// Automates new microservice project setup in Azure DevOps
var exec = require("child_process").execSync;

var config = {
    org: process.env.AZURE_DEVOPS_ORG || "https://dev.azure.com/your-org",
    project: process.env.AZURE_DEVOPS_PROJECT || "your-project",
    serviceName: process.argv[2],
    team: process.argv[3] || "default-team"
};

if (!config.serviceName) {
    console.error("Usage: node setup-project.js <service-name> [team-name]");
    process.exit(1);
}

function run(cmd) {
    console.log("  > " + cmd.substring(0, 100) + (cmd.length > 100 ? "..." : ""));
    try {
        var result = exec(cmd, { encoding: "utf8", timeout: 30000 });
        return result.trim();
    } catch (err) {
        console.error("  FAILED: " + err.stderr);
        return null;
    }
}

function runJson(cmd) {
    var result = run(cmd + " --output json");
    if (!result) { return null; }
    try { return JSON.parse(result); } catch (e) { return null; }
}

console.log("\n=== Setting up: " + config.serviceName + " ===\n");

// Step 1: Create repository
console.log("[1/6] Creating repository...");
var repo = runJson("az repos create --name " + config.serviceName +
    " --org " + config.org + " -p " + config.project);
if (!repo) { console.error("Failed to create repo"); process.exit(1); }
console.log("  Repo ID: " + repo.id);
console.log("  Clone URL: " + repo.remoteUrl);

// Step 2: Set up branch policies
console.log("\n[2/6] Configuring branch policies...");
run("az repos policy approver-count create" +
    " --repository-id " + repo.id +
    " --branch main" +
    " --minimum-approver-count 2" +
    " --creator-vote-counts false" +
    " --allow-downvotes false" +
    " --reset-on-source-push true" +
    " --blocking true --enabled true" +
    " --org " + config.org + " -p " + config.project);

run("az repos policy comment-required create" +
    " --repository-id " + repo.id +
    " --branch main" +
    " --blocking true --enabled true" +
    " --org " + config.org + " -p " + config.project);

// Step 3: Create area path
console.log("\n[3/6] Creating area path...");
run("az boards area project create" +
    " --name " + config.serviceName +
    " --org " + config.org + " -p " + config.project);

// Step 4: Create initial work items
console.log("\n[4/6] Creating initial work items...");
var stories = [
    "Set up CI/CD pipeline for " + config.serviceName,
    "Implement health check endpoint",
    "Add structured logging",
    "Configure monitoring and alerts",
    "Write API documentation"
];

var storyIds = [];
stories.forEach(function (title) {
    var item = runJson("az boards work-item create" +
        " --type \"User Story\"" +
        " --title \"" + title + "\"" +
        " --area " + config.project + "\\\\" + config.serviceName +
        " --org " + config.org + " -p " + config.project);
    if (item) {
        storyIds.push(item.id);
        console.log("  Created #" + item.id + ": " + title);
    }
});

// Step 5: Create pipeline definition placeholder
console.log("\n[5/6] Pipeline setup...");
console.log("  NOTE: Create azure-pipelines.yml in the repo, then run:");
console.log("  az pipelines create --name " + config.serviceName + "-ci" +
    " --repository " + config.serviceName +
    " --branch main --yml-path azure-pipelines.yml");

// Step 6: Summary
console.log("\n[6/6] Setup complete!");
console.log("\n=== Summary ===");
console.log("  Repository: " + config.serviceName);
console.log("  Clone: git clone " + repo.remoteUrl);
console.log("  Area: " + config.project + "\\" + config.serviceName);
console.log("  Work items: " + storyIds.length + " stories created");
console.log("  Branch policies: minimum 2 reviewers, comment resolution required");
console.log("\nNext steps:");
console.log("  1. Clone the repo and push initial code");
console.log("  2. Add azure-pipelines.yml");
console.log("  3. Create the pipeline with the command above");

Run it:

node setup-project.js payment-service backend-team

Common Issues and Troubleshooting

"az devops: command not found" after installing the extension

az devops: 'devops' is not in the 'az' command group

The extension may not have installed correctly. Run az extension list to verify azure-devops appears. If not, reinstall with az extension add --name azure-devops --upgrade. On some systems, you need to restart your shell after installing extensions.

Authentication fails with "TF400813: The user is not authorized"

TF400813: The user '' is not authorized to access this resource.

Your PAT may have expired or lacks the required scopes. For CLI usage, create a PAT with Full access scope or at minimum: Code (Read & Write), Build (Read & Execute), Work Items (Read & Write), and Project and Team (Read). Verify the organization URL matches the PAT's organization.

WIQL query returns error "The query contains a field that does not exist"

VS402337: The query contains one or more fields that do not exist.

Field names in WIQL are case-sensitive and must use the reference name, not the display name. Use Microsoft.VSTS.Common.Priority not Priority. Run az boards work-item show --id <any-id> --output json | jq '.fields | keys' to see the exact field reference names available in your project.

JQ parsing fails on Windows

On Windows, single quotes in jq commands do not work in CMD or PowerShell. Use double quotes and escape inner quotes:

# PowerShell
az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me" --output json | jq -r ".[].id"

# Or use --query with JMESPath instead of jq
az boards query --wiql "SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me" --query "[].id" --output tsv

Commands hang when default org/project not set

If you forget to set defaults and do not pass --org and -p, the CLI may prompt interactively or hang waiting for input. Always set defaults or explicitly pass organization and project in scripts. Add --only-show-errors to suppress non-error output in automated scripts.

Best Practices

  • Set defaults for your primary project. Running az devops configure --defaults once saves typing --org and -p on every command. For scripts that target multiple projects, explicitly pass the flags.

  • Use --output table for interactive use, --output json for scripts. Table output is readable for humans. JSON output pipes cleanly into jq for field extraction and transformation.

  • Alias frequently used commands. Add aliases to your .bashrc or .zshrc for commands you run daily. Examples: alias wi='az boards work-item show --id', alias prs='az repos pr list --status active -o table'.

  • Combine CLI with jq for powerful queries. The Azure DevOps CLI returns structured JSON. Pipe it into jq for filtering, transformation, and extraction that goes beyond what --query JMESPath can do.

  • Use the CLI in Git hooks. Post-commit hooks can auto-link commits to work items. Pre-push hooks can verify the target branch has an open PR. The CLI makes these Git integrations straightforward.

  • Store PATs in environment variables, never in scripts. Use AZURE_DEVOPS_EXT_PAT for authentication in scripts. Never hard-code tokens in shell scripts or commit them to repositories.

  • Prefer --query JMESPath over jq for simple extractions. The --query flag is built into Azure CLI and works on all platforms without additional tools. Use jq only when you need complex transformations.

References

Powered by Contentful