Azure DevOps CLI: Command-Line Productivity
Boost productivity with the Azure DevOps CLI for work items, pipelines, PRs, and automated development workflows
Azure DevOps CLI: Command-Line Productivity
The Azure DevOps CLI extension transforms how you interact with Azure DevOps by bringing work items, pipelines, pull requests, and repository management directly into your terminal. Instead of clicking through the web UI for routine tasks, you can script entire workflows, chain commands together, and automate repetitive operations in seconds. If you spend any meaningful amount of time in Azure DevOps, the CLI pays for itself on day one.
Prerequisites
- Azure CLI installed (version 2.30.0 or later)
- An Azure DevOps organization and project
- A Personal Access Token (PAT) or Azure AD authentication configured
- Node.js v14 or later (for the Node.js integration examples)
- Basic familiarity with bash scripting and command-line tools
jqinstalled for JSON processing (optional but highly recommended)
Installing the Azure DevOps CLI Extension
The Azure DevOps CLI is not part of the base Azure CLI installation. It ships as an extension that you install separately:
# Install the Azure CLI (if not already installed)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Install the Azure DevOps extension
az extension add --name azure-devops
# Verify the installation
az devops --help
If you already have an older version installed, upgrade it:
az extension update --name azure-devops
One thing I see developers trip over is version conflicts. If az extension add fails, remove the old version first with az extension remove --name azure-devops and then reinstall. Clean installs resolve most extension issues.
Authentication and Configuration
Before running any commands, you need to authenticate and set default configuration values. There are two main authentication paths.
Personal Access Token (PAT)
The simplest approach is a PAT. Generate one in Azure DevOps under User Settings > Personal Access Tokens with the scopes you need (Full access works for development, but scope it down for CI/CD):
# Login with a PAT (interactive prompt)
az devops login
# Or pass it via environment variable
export AZURE_DEVOPS_EXT_PAT=your-pat-token-here
Azure AD Authentication
If your organization uses Azure AD, you can authenticate through the standard Azure CLI login:
az login
Setting Defaults
Setting defaults eliminates the need to pass --org and --project on every single command. This is critical for productivity:
# Set your default organization and project
az devops configure --defaults \
organization=https://dev.azure.com/your-org \
project=your-project
# Verify your defaults
az devops configure --list
I always set these in my shell profile so every new terminal session is ready to go. Add this to your .bashrc or .zshrc:
export AZURE_DEVOPS_EXT_PAT="your-pat-here"
az devops configure --defaults \
organization=https://dev.azure.com/your-org \
project=your-project 2>/dev/null
Work Item Commands
Work items are the bread and butter of Azure DevOps project management. The CLI makes it fast to create, update, and query them without leaving your editor.
Creating Work Items
# Create a user story
az boards work-item create \
--type "User Story" \
--title "Implement user authentication API" \
--assigned-to "[email protected]" \
--area "MyProject\Backend" \
--iteration "MyProject\Sprint 42"
# Create a bug with description
az boards work-item create \
--type "Bug" \
--title "Login endpoint returns 500 on expired tokens" \
--description "When a user submits an expired JWT, the /api/login endpoint throws an unhandled exception instead of returning 401." \
--assigned-to "[email protected]" \
--fields "Microsoft.VSTS.Common.Priority=1" "Microsoft.VSTS.Common.Severity=2 - High"
# Create a task linked to a parent story
az boards work-item create \
--type "Task" \
--title "Add token expiration check middleware" \
--assigned-to "[email protected]" \
--fields "System.Parent=12345"
Updating Work Items
# Update the state of a work item
az boards work-item update \
--id 12345 \
--state "Active"
# Update multiple fields at once
az boards work-item update \
--id 12345 \
--fields "[email protected]" \
"Microsoft.VSTS.Scheduling.RemainingWork=4" \
"System.State=Active"
# Add a comment to a work item
az boards work-item update \
--id 12345 \
--discussion "Completed initial implementation. PR #789 is ready for review."
Querying Work Items
WIQL (Work Item Query Language) is how you search for work items from the CLI. It is SQL-like and quite powerful:
# Query work items assigned to you
az boards query \
--wiql "SELECT [System.Id], [System.Title], [System.State] \
FROM WorkItems \
WHERE [System.AssignedTo] = @Me \
AND [System.State] <> 'Closed' \
ORDER BY [System.ChangedDate] DESC"
# Show details for a specific work item
az boards work-item show --id 12345
# Show only specific fields
az boards work-item show --id 12345 \
--fields "System.Title" "System.State" "System.AssignedTo"
You can also run saved queries by ID, which is useful when your team has standardized queries in the web UI:
az boards query --id "your-saved-query-guid"
Pipeline Commands
Managing pipelines from the CLI is where real time savings happen, especially when you are iterating on pipeline configurations.
Listing and Running Pipelines
# List all pipelines in the project
az pipelines list --output table
# Show details for a specific pipeline
az pipelines show --id 42
# Run a pipeline
az pipelines run --id 42
# Run a pipeline on a specific branch with parameters
az pipelines run \
--id 42 \
--branch feature/auth-module \
--parameters "environment=staging" "runTests=true"
# Run and wait for completion (useful in scripts)
az pipelines run --id 42 --branch main --open
Monitoring Builds
# List recent builds
az pipelines build list --top 10 --output table
# Show a specific build
az pipelines build show --id 5678
# List builds for a specific pipeline
az pipelines build list --definition-ids 42 --top 5 --output table
# Show build logs
az pipelines runs show --id 5678 --open
One pattern I use constantly is watching for a build to finish after triggering it:
# Trigger a build and capture the build ID
BUILD_ID=$(az pipelines run --id 42 --branch main --query "id" -o tsv)
# Poll until complete
while true; do
STATUS=$(az pipelines build show --id $BUILD_ID --query "status" -o tsv)
echo "Build $BUILD_ID: $STATUS"
if [ "$STATUS" = "completed" ]; then
RESULT=$(az pipelines build show --id $BUILD_ID --query "result" -o tsv)
echo "Result: $RESULT"
break
fi
sleep 30
done
Pull Request Commands
Pull requests are another area where the CLI shines. Creating a PR with the right reviewers, linked work items, and proper description from the command line is fast once you have the commands memorized.
Creating Pull Requests
# Create a basic PR
az repos pr create \
--source-branch feature/auth-module \
--target-branch main \
--title "Add JWT authentication middleware" \
--description "Implements token validation middleware with refresh token support."
# Create a PR with reviewers and work item links
az repos pr create \
--source-branch feature/auth-module \
--target-branch main \
--title "Add JWT authentication middleware" \
--description "Implements token validation middleware with refresh token support." \
--reviewers "[email protected]" "[email protected]" \
--work-items 12345 12346
# Create a draft PR
az repos pr create \
--source-branch feature/auth-module \
--target-branch main \
--title "[DRAFT] Add JWT authentication middleware" \
--draft true
Reviewing and Approving PRs
# List open PRs
az repos pr list --status active --output table
# List PRs assigned to you for review
az repos pr list --reviewer-id "[email protected]" --status active --output table
# Show PR details
az repos pr show --id 789
# Approve a PR
az repos pr set-vote --id 789 --vote approve
# Approve with message
az repos pr set-vote --id 789 --vote approve
# Request changes
az repos pr set-vote --id 789 --vote reject
# Complete (merge) a PR
az repos pr update --id 789 --status completed
# Complete with squash merge
az repos pr update --id 789 --status completed --squash true --delete-source-branch true
PR Policies and Comments
# List PR reviewers
az repos pr reviewer list --id 789
# Add a reviewer
az repos pr reviewer add --id 789 --reviewers "[email protected]"
# List PR comments/threads
az repos pr list --id 789 --output table
Repository Commands
# List repositories in the project
az repos list --output table
# Show repository details
az repos show --repository my-api
# Create a new repository
az repos create --name "new-microservice"
# Clone a repository (outputs the clone URL)
CLONE_URL=$(az repos show --repository my-api --query "remoteUrl" -o tsv)
git clone $CLONE_URL
# List branches
az repos ref list --repository my-api --filter heads/ --output table
# Delete a branch
az repos ref delete \
--name "refs/heads/feature/old-branch" \
--repository my-api \
--object-id $(az repos ref list --repository my-api --filter heads/feature/old-branch --query "[0].objectId" -o tsv)
Project and Team Management
# List all projects in the organization
az devops project list --output table
# Show project details
az devops project show --project my-project
# List teams
az devops team list --output table
# Show team members
az devops team list-member --team "Backend Team"
# Create a new project
az devops project create \
--name "new-project" \
--description "Microservice for payment processing" \
--source-control git \
--process agile
Output Formatting
The Azure CLI supports multiple output formats, and choosing the right one for the task matters a lot.
Table Format
Best for human-readable output in the terminal:
az boards query \
--wiql "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.State] = 'Active'" \
--output table
JSON Format
Best for scripting and piping to jq:
az boards work-item show --id 12345 --output json | jq '.fields["System.Title"]'
TSV Format
Best for piping to other commands and assigning to variables:
TITLE=$(az boards work-item show --id 12345 --query "fields.\"System.Title\"" -o tsv)
echo "Working on: $TITLE"
JMESPath Queries
The --query parameter uses JMESPath syntax to filter output before it reaches your terminal. This is invaluable:
# Get just the IDs of active work items
az boards query \
--wiql "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'" \
--query "[].id" -o tsv
# Get pipeline names and IDs
az pipelines list --query "[].{Name:name, ID:id}" -o table
Scripting with the CLI
The real power of the CLI emerges when you combine commands into scripts. Here are patterns I use regularly.
Sprint Standup Report
#!/bin/bash
# standup-report.sh - Generate a standup report from Azure DevOps
echo "=== Standup Report - $(date +%Y-%m-%d) ==="
echo ""
echo "--- In Progress ---"
az boards query \
--wiql "SELECT [System.Id], [System.Title], [System.WorkItemType] \
FROM WorkItems \
WHERE [System.AssignedTo] = @Me \
AND [System.State] = 'Active' \
ORDER BY [Microsoft.VSTS.Common.Priority] ASC" \
--output table
echo ""
echo "--- Completed Yesterday ---"
az boards query \
--wiql "SELECT [System.Id], [System.Title], [System.WorkItemType] \
FROM WorkItems \
WHERE [System.AssignedTo] = @Me \
AND [System.State] = 'Closed' \
AND [System.ChangedDate] >= @Today - 1 \
ORDER BY [System.ChangedDate] DESC" \
--output table
echo ""
echo "--- Open PRs ---"
az repos pr list \
--creator "[email protected]" \
--status active \
--output table
Bulk Work Item Creation
#!/bin/bash
# create-tasks.sh - Create multiple tasks from a file
PARENT_ID=$1
TASK_FILE=$2
if [ -z "$PARENT_ID" ] || [ -z "$TASK_FILE" ]; then
echo "Usage: ./create-tasks.sh <parent-work-item-id> <task-file.txt>"
exit 1
fi
while IFS= read -r task; do
[ -z "$task" ] && continue
echo "Creating task: $task"
az boards work-item create \
--type "Task" \
--title "$task" \
--fields "System.Parent=$PARENT_ID" \
--output table
done < "$TASK_FILE"
Creating Aliases for Common Workflows
Shell aliases and functions eliminate keystrokes for commands you run dozens of times a day:
# Add to .bashrc or .zshrc
# Quick work item lookup
alias wi='az boards work-item show --id'
# List my active items
alias mywork='az boards query --wiql "SELECT [System.Id],[System.Title],[System.State] FROM WorkItems WHERE [System.AssignedTo]=@Me AND [System.State]<>\"Closed\" ORDER BY [Microsoft.VSTS.Common.Priority] ASC" -o table'
# Quick PR creation from current branch
function newpr() {
local branch=$(git branch --show-current)
local target=${1:-main}
local title=${2:-$branch}
az repos pr create \
--source-branch "$branch" \
--target-branch "$target" \
--title "$title" \
--open
}
# Trigger a pipeline and watch it
function runpipe() {
local pipeline_id=$1
local branch=${2:-main}
local build_id=$(az pipelines run --id "$pipeline_id" --branch "$branch" --query "id" -o tsv)
echo "Started build $build_id"
echo "Watching..."
while true; do
local status=$(az pipelines build show --id "$build_id" --query "status" -o tsv)
if [ "$status" = "completed" ]; then
local result=$(az pipelines build show --id "$build_id" --query "result" -o tsv)
echo "Build $build_id completed: $result"
return
fi
sleep 15
done
}
# Move a work item to a new state
function move() {
az boards work-item update --id "$1" --state "$2" --output table
}
Integrating CLI with Node.js child_process
For more complex automation, wrapping the CLI in Node.js gives you the full power of a programming language. This is the approach I use for our internal tooling:
var childProcess = require("child_process");
var util = require("util");
var exec = util.promisify(childProcess.exec);
/**
* Execute an Azure DevOps CLI command and return parsed JSON
*/
function azDevOps(command) {
var fullCommand = "az " + command + " -o json";
return exec(fullCommand, { maxBuffer: 10 * 1024 * 1024 })
.then(function (result) {
if (result.stderr && result.stderr.trim()) {
console.warn("CLI warning:", result.stderr.trim());
}
return JSON.parse(result.stdout);
})
.catch(function (err) {
console.error("CLI error:", err.message);
throw err;
});
}
/**
* Get all active work items assigned to a specific user
*/
function getActiveWorkItems(assignee) {
var wiql =
"SELECT [System.Id],[System.Title],[System.State],[System.WorkItemType] " +
"FROM WorkItems " +
"WHERE [System.AssignedTo]='" + assignee + "' " +
"AND [System.State]<>'Closed' " +
"ORDER BY [Microsoft.VSTS.Common.Priority] ASC";
return azDevOps('boards query --wiql "' + wiql + '"');
}
/**
* Create a work item with error handling
*/
function createWorkItem(type, title, fields) {
var command = 'boards work-item create --type "' + type + '" --title "' + title + '"';
if (fields && Object.keys(fields).length > 0) {
var fieldArgs = Object.keys(fields).map(function (key) {
return '"' + key + "=" + fields[key] + '"';
});
command += " --fields " + fieldArgs.join(" ");
}
return azDevOps(command);
}
/**
* Get pipeline status with build details
*/
function getPipelineStatus(pipelineId, top) {
var count = top || 5;
return azDevOps(
"pipelines build list --definition-ids " + pipelineId + " --top " + count
);
}
/**
* Trigger a pipeline run and poll for completion
*/
function runPipelineAndWait(pipelineId, branch, pollInterval) {
var interval = pollInterval || 15000;
return azDevOps(
'pipelines run --id ' + pipelineId + ' --branch "' + branch + '"'
).then(function (run) {
var buildId = run.id;
console.log("Started build " + buildId + " on branch " + branch);
return new Promise(function (resolve, reject) {
var timer = setInterval(function () {
azDevOps("pipelines build show --id " + buildId)
.then(function (build) {
console.log("Build " + buildId + ": " + build.status);
if (build.status === "completed") {
clearInterval(timer);
if (build.result === "succeeded") {
resolve(build);
} else {
reject(new Error("Build failed with result: " + build.result));
}
}
})
.catch(function (err) {
clearInterval(timer);
reject(err);
});
}, interval);
});
});
}
// Example usage
function main() {
console.log("Fetching active work items...");
getActiveWorkItems("[email protected]")
.then(function (items) {
console.log("Found " + items.length + " active items");
items.forEach(function (item) {
console.log(" [" + item.id + "] " + item.fields["System.Title"]);
});
return getPipelineStatus(42, 3);
})
.then(function (builds) {
console.log("\nRecent builds:");
builds.forEach(function (build) {
console.log(
" Build " + build.id + ": " + build.status + " (" + build.result + ")"
);
});
})
.catch(function (err) {
console.error("Error:", err.message);
process.exit(1);
});
}
main();
Bulk Operations with jq
For bulk operations, piping CLI output through jq is extremely effective. These are real patterns from my daily workflow:
# Close all resolved work items older than 30 days
az boards query \
--wiql "SELECT [System.Id] FROM WorkItems \
WHERE [System.State] = 'Resolved' \
AND [System.ChangedDate] < @Today - 30" \
-o json | jq -r '.[].id' | while read id; do
echo "Closing work item $id"
az boards work-item update --id "$id" --state "Closed" --output none
done
# Export all active bugs to CSV
echo "ID,Title,Priority,AssignedTo" > bugs.csv
az boards query \
--wiql "SELECT [System.Id] FROM WorkItems \
WHERE [System.WorkItemType] = 'Bug' \
AND [System.State] = 'Active'" \
-o json | jq -r '.[].id' | while read id; do
az boards work-item show --id "$id" -o json | \
jq -r '[
.id,
.fields["System.Title"],
.fields["Microsoft.VSTS.Common.Priority"],
.fields["System.AssignedTo"].displayName // "Unassigned"
] | @csv'
done >> bugs.csv
# Reassign all items from one developer to another
az boards query \
--wiql "SELECT [System.Id] FROM WorkItems \
WHERE [System.AssignedTo] = '[email protected]' \
AND [System.State] <> 'Closed'" \
-o json | jq -r '.[].id' | while read id; do
az boards work-item update --id "$id" \
--fields "[email protected]" --output none
echo "Reassigned $id"
done
# Get all failed builds from the last week with error summaries
az pipelines build list \
--result failed \
--top 50 \
-o json | jq -r '.[] | select(.finishTime > (now - 604800 | todate)) | "\(.id) \(.definition.name) \(.sourceBranch)"'
CI/CD Pipeline Integration
You can use the Azure DevOps CLI inside your pipelines themselves to create cross-pipeline orchestration, update work items automatically, or trigger downstream processes:
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Update work items on successful build'
inputs:
azureSubscription: 'your-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az extension add --name azure-devops
# Get work items linked to commits in this build
COMMIT_MSG=$(git log -1 --pretty=%B)
WORK_ITEM_ID=$(echo "$COMMIT_MSG" | grep -oP '#\K[0-9]+' | head -1)
if [ -n "$WORK_ITEM_ID" ]; then
echo "Updating work item #$WORK_ITEM_ID"
az boards work-item update \
--id "$WORK_ITEM_ID" \
--discussion "Build $(Build.BuildNumber) succeeded. Deployed to staging." \
--org "$(System.CollectionUri)" \
--project "$(System.TeamProject)"
fi
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
- task: AzureCLI@2
displayName: 'Trigger deployment pipeline'
inputs:
azureSubscription: 'your-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az extension add --name azure-devops
az pipelines run \
--id $(DEPLOY_PIPELINE_ID) \
--branch "$(Build.SourceBranch)" \
--parameters "buildNumber=$(Build.BuildNumber)" "environment=staging" \
--org "$(System.CollectionUri)" \
--project "$(System.TeamProject)"
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
Complete Working Example
Here is a complete set of scripts that automate a daily development workflow. The system includes a bash wrapper script and a Node.js orchestrator.
devflow.sh — Daily Workflow Automation
#!/bin/bash
# devflow.sh - Daily development workflow automation
# Usage: ./devflow.sh [standup|start|pr|release]
set -e
ACTION=$1
BRANCH=$(git branch --show-current 2>/dev/null || echo "none")
function standup() {
echo "========================================="
echo " STANDUP REPORT - $(date +%Y-%m-%d)"
echo "========================================="
echo ""
echo "IN PROGRESS:"
az boards query \
--wiql "SELECT [System.Id],[System.Title],[System.WorkItemType] \
FROM WorkItems WHERE [System.AssignedTo]=@Me \
AND [System.State]='Active'" \
-o table 2>/dev/null || echo " (none)"
echo ""
echo "COMPLETED SINCE YESTERDAY:"
az boards query \
--wiql "SELECT [System.Id],[System.Title] FROM WorkItems \
WHERE [System.AssignedTo]=@Me \
AND [System.State]='Closed' \
AND [System.ChangedDate]>=@Today-1" \
-o table 2>/dev/null || echo " (none)"
echo ""
echo "OPEN PULL REQUESTS:"
az repos pr list --creator "$(az devops user show --query 'user.mailAddress' -o tsv 2>/dev/null)" \
--status active -o table 2>/dev/null || echo " (none)"
echo ""
echo "RECENT BUILDS:"
az pipelines build list --top 5 -o table 2>/dev/null || echo " (none)"
}
function start_work() {
local WORK_ITEM_ID=$2
if [ -z "$WORK_ITEM_ID" ]; then
echo "Usage: ./devflow.sh start <work-item-id>"
exit 1
fi
# Get work item title for branch name
local TITLE=$(az boards work-item show --id "$WORK_ITEM_ID" \
--query "fields.\"System.Title\"" -o tsv)
local SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | head -c 50)
local BRANCH_NAME="feature/${WORK_ITEM_ID}-${SLUG}"
echo "Starting work on #${WORK_ITEM_ID}: ${TITLE}"
echo "Branch: ${BRANCH_NAME}"
# Create and checkout branch
git checkout -b "$BRANCH_NAME" main
git push -u origin "$BRANCH_NAME"
# Move work item to Active
az boards work-item update --id "$WORK_ITEM_ID" --state "Active" --output none
az boards work-item update --id "$WORK_ITEM_ID" \
--discussion "Started development on branch ${BRANCH_NAME}" --output none
echo "Ready to work. Branch created and work item activated."
}
function create_pr() {
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "none" ]; then
echo "Error: switch to a feature branch first."
exit 1
fi
# Extract work item ID from branch name
local WI_ID=$(echo "$BRANCH" | grep -oP '\d+' | head -1)
# Get work item title
local TITLE=""
if [ -n "$WI_ID" ]; then
TITLE=$(az boards work-item show --id "$WI_ID" \
--query "fields.\"System.Title\"" -o tsv 2>/dev/null)
fi
if [ -z "$TITLE" ]; then
TITLE="$BRANCH"
fi
# Push latest changes
git push origin "$BRANCH"
# Create the PR
local PR_CMD="az repos pr create --source-branch $BRANCH --target-branch main --title \"$TITLE\""
if [ -n "$WI_ID" ]; then
PR_CMD="$PR_CMD --work-items $WI_ID"
fi
eval "$PR_CMD" --output table
echo "Pull request created."
}
function release() {
local PIPELINE_ID=$2
if [ -z "$PIPELINE_ID" ]; then
echo "Usage: ./devflow.sh release <pipeline-id>"
exit 1
fi
echo "Triggering release pipeline $PIPELINE_ID on main..."
local BUILD_ID=$(az pipelines run --id "$PIPELINE_ID" --branch main --query "id" -o tsv)
echo "Build $BUILD_ID started. Monitoring..."
while true; do
local STATUS=$(az pipelines build show --id "$BUILD_ID" --query "status" -o tsv)
local RESULT=$(az pipelines build show --id "$BUILD_ID" --query "result" -o tsv)
echo " $(date +%H:%M:%S) - Status: $STATUS, Result: $RESULT"
if [ "$STATUS" = "completed" ]; then
if [ "$RESULT" = "succeeded" ]; then
echo "Release build succeeded."
else
echo "Release build failed with result: $RESULT"
exit 1
fi
break
fi
sleep 20
done
}
case "$ACTION" in
standup) standup ;;
start) start_work "$@" ;;
pr) create_pr ;;
release) release "$@" ;;
*)
echo "Usage: ./devflow.sh [standup|start|pr|release]"
echo " standup - Generate daily standup report"
echo " start <work-item-id> - Create branch and activate work item"
echo " pr - Create PR from current branch"
echo " release <pipeline-id> - Trigger and monitor release pipeline"
;;
esac
devflow-node.js — Node.js Orchestrator
var childProcess = require("child_process");
var util = require("util");
var fs = require("fs");
var exec = util.promisify(childProcess.exec);
function runAz(command) {
return exec("az " + command + " -o json", {
maxBuffer: 10 * 1024 * 1024,
}).then(function (result) {
try {
return JSON.parse(result.stdout);
} catch (e) {
return result.stdout.trim();
}
});
}
function runShell(command) {
return exec(command).then(function (result) {
return result.stdout.trim();
});
}
/**
* Generate a full sprint health report
*/
function sprintHealthReport() {
var report = {
generated: new Date().toISOString(),
activeItems: [],
blockedItems: [],
openPRs: [],
failedBuilds: [],
};
return Promise.all([
runAz(
'boards query --wiql "SELECT [System.Id],[System.Title],[System.State] FROM WorkItems WHERE [System.AssignedTo]=@Me AND [System.State]=\'Active\'"'
),
runAz(
'boards query --wiql "SELECT [System.Id],[System.Title] FROM WorkItems WHERE [System.AssignedTo]=@Me AND [System.Tags] CONTAINS \'blocked\'"'
),
runAz("repos pr list --status active"),
runAz("pipelines build list --result failed --top 10"),
])
.then(function (results) {
report.activeItems = results[0] || [];
report.blockedItems = results[1] || [];
report.openPRs = results[2] || [];
report.failedBuilds = results[3] || [];
report.summary = {
activeCount: report.activeItems.length,
blockedCount: report.blockedItems.length,
openPRCount: report.openPRs.length,
failedBuildCount: report.failedBuilds.length,
};
return report;
});
}
/**
* Auto-create tasks from a template for a user story
*/
function createTasksFromTemplate(storyId, templatePath) {
var template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
var promises = template.tasks.map(function (task) {
return runAz(
'boards work-item create --type "Task" --title "' +
task.title +
'" --fields "System.Parent=' +
storyId +
'" "Microsoft.VSTS.Scheduling.RemainingWork=' +
task.hours +
'"'
).then(function (result) {
console.log("Created task: " + task.title + " (ID: " + result.id + ")");
return result;
});
});
return Promise.all(promises);
}
/**
* Find and close stale PRs
*/
function closeStalePRs(daysOld) {
var cutoff = new Date();
cutoff.setDate(cutoff.getDate() - daysOld);
return runAz("repos pr list --status active").then(function (prs) {
var stale = prs.filter(function (pr) {
return new Date(pr.creationDate) < cutoff;
});
console.log("Found " + stale.length + " PRs older than " + daysOld + " days");
var closePromises = stale.map(function (pr) {
console.log(
" Abandoning PR #" + pr.pullRequestId + ": " + pr.title
);
return runAz(
"repos pr update --id " + pr.pullRequestId + " --status abandoned"
);
});
return Promise.all(closePromises);
});
}
// CLI interface
var action = process.argv[2];
switch (action) {
case "health":
sprintHealthReport().then(function (report) {
console.log(JSON.stringify(report, null, 2));
});
break;
case "tasks":
var storyId = process.argv[3];
var templatePath = process.argv[4];
if (!storyId || !templatePath) {
console.log("Usage: node devflow-node.js tasks <story-id> <template.json>");
process.exit(1);
}
createTasksFromTemplate(storyId, templatePath);
break;
case "stale-prs":
var days = parseInt(process.argv[3]) || 14;
closeStalePRs(days);
break;
default:
console.log("Usage: node devflow-node.js [health|tasks|stale-prs]");
console.log(" health - Sprint health report");
console.log(" tasks <id> <file> - Create tasks from template");
console.log(" stale-prs [days] - Close stale PRs (default: 14 days)");
}
Task Template (task-template.json)
{
"tasks": [
{ "title": "Design API contract", "hours": 4 },
{ "title": "Implement endpoint logic", "hours": 8 },
{ "title": "Write unit tests", "hours": 4 },
{ "title": "Write integration tests", "hours": 4 },
{ "title": "Update API documentation", "hours": 2 },
{ "title": "Code review and revisions", "hours": 2 }
]
}
Common Issues and Troubleshooting
1. "The resource cannot be found" on Work Item Queries
This almost always means your default project is not set or is set to the wrong project. Run az devops configure --list and verify both organization and project are correct. WIQL queries do not cross project boundaries by default.
2. PAT Token Expiration with No Clear Error
When your PAT expires, the CLI returns vague authentication errors rather than telling you the token expired. If commands that previously worked suddenly return 401 or TF400813 errors, generate a new PAT first before debugging anything else. I set a calendar reminder 7 days before expiration.
3. Output Truncation in Pipelines
When running the CLI inside Azure Pipelines, the output buffer can fill up if you are querying large result sets. Always use --top to limit results, and use --query with JMESPath to reduce the payload before it hits stdout. In Node.js, increase maxBuffer in the exec options.
4. WIQL Quoting Issues on Windows vs. Linux
WIQL queries with single quotes inside double quotes behave differently on Windows CMD vs. bash. On Windows, you may need to escape differently or use a WIQL file:
# Save WIQL to a file to avoid quoting issues
echo 'SELECT [System.Id] FROM WorkItems WHERE [System.State] = "Active"' > query.wiql
az boards query --wiql @query.wiql
5. Extension Version Conflicts After CLI Update
After updating the Azure CLI itself, the DevOps extension can break due to API version mismatches. The fix is straightforward: remove and reinstall the extension. Run az extension remove --name azure-devops && az extension add --name azure-devops.
6. Rate Limiting on Bulk Operations
Azure DevOps APIs have rate limits. If you are running bulk operations (updating hundreds of work items, for instance), add a sleep between calls. A 200ms delay between requests keeps you well under the limits while still being fast.
Best Practices
Always set default organization and project. Typing
--organd--projecton every command is a waste of keystrokes and a source of errors. Set them once withaz devops configure --defaults.Use JMESPath queries to reduce output. Do not pipe massive JSON blobs through multiple tools when
--querycan extract exactly what you need at the source. Learn the JMESPath syntax — it pays dividends.Store your PAT in an environment variable, not in scripts. Never hardcode tokens. Use
AZURE_DEVOPS_EXT_PATand load it from a secrets manager or encrypted file in your shell profile.Add
--output nonefor scripted updates. When running bulk operations in a loop, suppress output with--output noneto avoid flooding the terminal and to speed up execution.Use
--detectfor repository-aware commands. The CLI can auto-detect the organization and project from your git remote URL. Pass--detect truewhen working inside a cloned Azure DevOps repository.Version your CLI scripts alongside your code. Keep automation scripts in a
tools/orscripts/directory in your repository. When the team shares these scripts, everyone benefits from the same automation.Wrap CLI calls in retry logic for CI/CD. Network blips happen. In pipeline scripts and Node.js wrappers, add retry logic with exponential backoff for transient failures.
Prefer TSV output for variable assignment. When capturing a single value into a bash variable,
-o tsvgives you a clean string without quotes or brackets. JSON output requires additional parsing.Scope PATs to minimum required permissions. For CI/CD service accounts, create PATs with only the scopes needed (e.g., Work Items Read/Write, Code Read, Build Read/Execute). Full access tokens are a security risk.