118 lines
4.3 KiB
Bash
Executable File
118 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Get all work items (tickets) between two branches in Azure DevOps.
|
|
#
|
|
# Reports tickets from TWO sources:
|
|
# 1. Formally linked work items (git commit <-> work item links)
|
|
# 2. Ticket numbers scraped from commit messages (this repo references
|
|
# tickets in text far more than it formally links them)
|
|
#
|
|
# Usage:
|
|
# ./azdo-tickets-between-branches.sh <baseBranch> <targetBranch>
|
|
#
|
|
# Example:
|
|
# ./azdo-tickets-between-branches.sh master release/5.4.0
|
|
#
|
|
# Requires: curl, jq
|
|
# Auth: set a Personal Access Token in AZDO_PAT (scopes: Code Read + Work Items Read).
|
|
|
|
set -euo pipefail
|
|
|
|
# ---- Config (override via env vars) -----------------------------------------
|
|
# Project collection base URL (host + collection), e.g. old visualstudio.com style.
|
|
COLLECTION_URL="${AZDO_COLLECTION_URL:-https://flashmobilevending.visualstudio.com/DefaultCollection}"
|
|
PROJECT="${AZDO_PROJECT:-GLU}"
|
|
REPO="${AZDO_REPO:-glu-flash-console}"
|
|
PAT="${AZDO_PAT:-}"
|
|
API_VERSION="7.1"
|
|
# Regex for ticket numbers in commit messages (this project uses 6-digit IDs).
|
|
TICKET_REGEX="${AZDO_TICKET_REGEX:-[0-9]{6}}"
|
|
# -----------------------------------------------------------------------------
|
|
|
|
BASE="${1:-}"
|
|
TARGET="${2:-}"
|
|
|
|
if [[ -z "$BASE" || -z "$TARGET" ]]; then
|
|
echo "Usage: $0 <baseBranch> <targetBranch>" >&2
|
|
echo "Example: $0 master release/5.4.0" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$PAT" ]]; then
|
|
echo "Error: set AZDO_PAT to a Personal Access Token (Code Read + Work Items Read)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
for cmd in curl jq; do
|
|
command -v "$cmd" >/dev/null 2>&1 || { echo "Error: '$cmd' is required." >&2; exit 1; }
|
|
done
|
|
|
|
GIT_URL="${COLLECTION_URL}/${PROJECT}/_apis/git/repositories/${REPO}"
|
|
WIT_URL="${COLLECTION_URL}/${PROJECT}/_apis/wit/workitems"
|
|
|
|
# ---- Step 1: commits on TARGET but not on BASE, with linked work items -------
|
|
commits_json=$(curl -sf -u ":${PAT}" \
|
|
"${GIT_URL}/commitsbatch?\$top=1000&api-version=${API_VERSION}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"itemVersion\": {\"versionType\":\"branch\",\"version\":\"${BASE}\"},
|
|
\"compareVersion\": {\"versionType\":\"branch\",\"version\":\"${TARGET}\"},
|
|
\"includeWorkItems\": true
|
|
}")
|
|
|
|
# 1a. Formally linked work item IDs
|
|
linked_ids=$(echo "$commits_json" | jq -r '[.value[].workItems[]?.id] | unique | .[]' || true)
|
|
|
|
# 1b. Ticket numbers scraped from commit messages.
|
|
# Strip "Merged PR NNNNN" first so PR ids aren't mistaken for tickets.
|
|
scraped_ids=$(echo "$commits_json" \
|
|
| jq -r '.value[].comment' \
|
|
| sed -E 's/[Mm]erged PR [0-9]+//g' \
|
|
| grep -oE "$TICKET_REGEX" \
|
|
| sort -u || true)
|
|
|
|
# Union of both sources
|
|
all_ids=$(printf '%s\n%s\n' "$linked_ids" "$scraped_ids" | grep -E '^[0-9]+$' | sort -un || true)
|
|
|
|
commit_count=$(echo "$commits_json" | jq '.value | length')
|
|
echo "Comparing '${TARGET}' against '${BASE}' (${commit_count} commits ahead)"
|
|
echo " Formally linked: $(echo "$linked_ids" | grep -c . || true)"
|
|
echo " Scraped from msgs: $(echo "$scraped_ids" | grep -c . || true)"
|
|
echo
|
|
|
|
if [[ -z "$all_ids" ]]; then
|
|
echo "No tickets found (no links and no ticket numbers in commit messages)."
|
|
exit 0
|
|
fi
|
|
|
|
# ---- Step 2: hydrate work item details --------------------------------------
|
|
ids_csv=$(echo "$all_ids" | paste -sd, -)
|
|
|
|
http_status=$(curl -s -o /tmp/azdo_wit.json -w "%{http_code}" -u ":${PAT}" \
|
|
"${WIT_URL}?ids=${ids_csv}&fields=System.Id,System.WorkItemType,System.State,System.Title&errorPolicy=omit&api-version=${API_VERSION}")
|
|
|
|
if [[ "$http_status" != "200" ]]; then
|
|
echo "Warning: could not fetch work item details (HTTP ${http_status})." >&2
|
|
if [[ "$http_status" == "401" || "$http_status" == "403" ]]; then
|
|
echo " Your PAT is likely missing the 'Work Items (Read)' scope." >&2
|
|
fi
|
|
echo " Falling back to ticket IDs only:" >&2
|
|
echo
|
|
echo "$all_ids"
|
|
echo
|
|
echo "View any of them at: ${COLLECTION_URL}/${PROJECT}/_workitems/edit/<id>"
|
|
exit 0
|
|
fi
|
|
|
|
# errorPolicy=omit returns null entries for ids that aren't real work items;
|
|
# filter those out so scraped false-positives disappear.
|
|
{
|
|
printf 'ID\tTYPE\tSTATE\tTITLE\n'
|
|
jq -r '.value[] | select(. != null)
|
|
| [ (.fields["System.Id"]|tostring),
|
|
.fields["System.WorkItemType"],
|
|
.fields["System.State"],
|
|
(.fields["System.Title"] // "") ]
|
|
| @tsv' /tmp/azdo_wit.json
|
|
} | column -t -s $'\t'
|