#!/bin/bash # Create Azure DevOps work items using Azure CLI with OAuth (no PAT required) # Requires: Azure CLI with devops extension set -e # Parse arguments SPEC_FILE="" ORGANIZATION="" PROJECT="" STORIES="all" AREA_PATH="" FROM_TASKS=false while [[ $# -gt 0 ]]; do case $1 in --spec-file) SPEC_FILE="$2" shift 2 ;; --organization) ORGANIZATION="$2" shift 2 ;; --project) PROJECT="$2" shift 2 ;; --stories) STORIES="$2" shift 2 ;; --area-path) AREA_PATH="$2" shift 2 ;; --from-tasks) FROM_TASKS=true shift ;; *) echo "Unknown option: $1" exit 1 ;; esac done # Validate required arguments if [[ -z "$SPEC_FILE" ]]; then echo "Error: --spec-file is required" exit 1 fi # Check if Azure CLI is installed if ! command -v az &> /dev/null; then echo "Error: Azure CLI not found. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli" exit 1 fi # Check if devops extension is installed if ! az extension list --output json | grep -q "azure-devops"; then echo "Installing Azure DevOps extension for Azure CLI..." az extension add --name azure-devops fi # Check authentication echo "Checking Azure authentication..." if ! az account show &> /dev/null; then echo "Not authenticated. Running 'az login' with OAuth..." az login --use-device-code fi # Config file path CONFIG_DIR="$HOME/.speckit" CONFIG_FILE="$CONFIG_DIR/ado-config.json" # Load saved config if exists if [[ -f "$CONFIG_FILE" ]]; then SAVED_ORG=$(jq -r '.Organization // empty' "$CONFIG_FILE") SAVED_PROJECT=$(jq -r '.Project // empty' "$CONFIG_FILE") SAVED_AREA=$(jq -r '.AreaPath // empty' "$CONFIG_FILE") fi # Get organization and project from command-line args, environment, or saved config if [[ -z "$ORGANIZATION" ]]; then ORGANIZATION="${AZURE_DEVOPS_ORG}" if [[ -z "$ORGANIZATION" ]] && [[ -n "$SAVED_ORG" ]]; then ORGANIZATION="$SAVED_ORG" fi fi if [[ -z "$PROJECT" ]]; then PROJECT="${AZURE_DEVOPS_PROJECT}" if [[ -z "$PROJECT" ]] && [[ -n "$SAVED_PROJECT" ]]; then PROJECT="$SAVED_PROJECT" fi fi if [[ -z "$AREA_PATH" ]] && [[ -n "$SAVED_AREA" ]]; then AREA_PATH="$SAVED_AREA" fi # Validate required parameters if [[ -z "$ORGANIZATION" ]]; then echo "Error: Organization parameter is required. Please provide --organization parameter." exit 1 fi if [[ -z "$PROJECT" ]]; then echo "Error: Project parameter is required. Please provide --project parameter." exit 1 fi if [[ -z "$AREA_PATH" ]]; then echo "Error: AreaPath parameter is required. Please provide --area-path parameter." exit 1 fi # Save configuration for future reference CONFIG_DIR="$HOME/.speckit" CONFIG_FILE="$CONFIG_DIR/ado-config.json" # Escape backslashes for JSON AREA_PATH_ESCAPED="${AREA_PATH//\\/\\\\}" mkdir -p "$CONFIG_DIR" cat > "$CONFIG_FILE" <&1) if [[ $? -eq 0 ]] && [[ ! "$result" =~ ERROR ]]; then work_item_id=$(echo "$result" | jq -r '.id') work_item_url="https://dev.azure.com/$ORGANIZATION/$PROJECT/_workitems/edit/$work_item_id" echo " [OK] Created work item #$work_item_id" echo " -> $work_item_url" echo "" CREATED_IDS+=("$work_item_id") CREATED_URLS+=("$work_item_url") CREATED_STORY_REFS+=("$story_ref") else echo " [FAIL] Failed to create work item" echo " Error: $result" echo "" fi break fi done else # Handle user story creation (original logic) for i in "${!STORY_NUMBERS[@]}"; do if [[ "${STORY_NUMBERS[$i]}" == "$selected" ]]; then num="${STORY_NUMBERS[$i]}" title="${STORY_TITLES[$i]}" priority="${STORY_PRIORITIES[$i]}" desc="${STORY_DESCRIPTIONS[$i]}" accept="${STORY_ACCEPTANCE[$i]}" work_item_title="User Story $num - $title" item_type="User Story" # Clean field values (remove newlines and escape quotes) # For title: double quotes for Azure CLI clean_title="${work_item_title//\"/\"\"}" clean_desc=$(echo "$desc" | tr '\n' ' ' | sed 's/"/\\"/g') clean_accept=$(echo "$accept" | tr '\n' ' ' | sed 's/"/\\"/g') tags="spec-kit;$FEATURE_NAME;user-story" echo "Creating User Story $num: $title..." # Build az command result=$(az boards work-item create \ --type "User Story" \ --title "$clean_title" \ --organization "https://dev.azure.com/$ORGANIZATION" \ --project "$PROJECT" \ --fields \ "System.Description=$clean_desc" \ "Microsoft.VSTS.Common.Priority=$priority" \ "System.Tags=$tags" \ "Microsoft.VSTS.Common.AcceptanceCriteria=$clean_accept" \ "System.AssignedTo=" \ ${AREA_PATH:+"System.AreaPath=$AREA_PATH"} \ --output json 2>&1) if [[ $? -eq 0 ]] && [[ ! "$result" =~ ERROR ]]; then work_item_id=$(echo "$result" | jq -r '.id') work_item_url="https://dev.azure.com/$ORGANIZATION/$PROJECT/_workitems/edit/$work_item_id" echo " [OK] Created work item #$work_item_id" echo " -> $work_item_url" echo "" CREATED_IDS+=("$work_item_id") CREATED_URLS+=("$work_item_url") else echo " [FAIL] Failed to create work item" echo " Error: $result" echo "" fi break fi done fi done # Link tasks to parent user stories if in FROM_TASKS mode if [[ "$FROM_TASKS" == true ]] && [[ ${#PARENT_MAPPING[@]} -gt 0 ]] && [[ ${#CREATED_IDS[@]} -gt 0 ]]; then echo "Linking tasks to parent user stories..." echo "" for i in "${!CREATED_IDS[@]}"; do story_ref="${CREATED_STORY_REFS[$i]}" if [[ -n "$story_ref" ]] && [[ -n "${PARENT_MAPPING[$story_ref]}" ]]; then parent_id="${PARENT_MAPPING[$story_ref]}" task_id="${CREATED_IDS[$i]}" echo -n " Linking Task #$task_id -> User Story #$parent_id..." link_result=$(az boards work-item relation add \ --id "$task_id" \ --relation-type "Parent" \ --target-id "$parent_id" \ --organization "https://dev.azure.com/$ORGANIZATION" \ --output json 2>&1) if [[ $? -eq 0 ]]; then echo " [OK]" else echo " [FAIL]" echo " Error: $link_result" fi fi done echo "" fi # Summary if [[ ${#CREATED_IDS[@]} -gt 0 ]]; then echo "" echo "==============================================" echo "[SUCCESS] Azure DevOps Sync Complete" echo "==============================================" echo "" echo "Organization: $ORGANIZATION" echo "Project: $PROJECT" echo "Feature: $FEATURE_NAME" if [[ "$FROM_TASKS" == true ]]; then echo "Created: ${#CREATED_IDS[@]} of ${#SELECTED_STORIES[@]} tasks" else echo "Created: ${#CREATED_IDS[@]} of ${#SELECTED_STORIES[@]} user stories" fi echo "" echo "Created Work Items:" echo "" for i in "${!CREATED_IDS[@]}"; do idx=$((i)) echo " [${SELECTED_STORIES[$idx]}] ${STORY_TITLES[$idx]} (P${STORY_PRIORITIES[$idx]})" echo " Work Item: #${CREATED_IDS[$i]}" echo " Link: ${CREATED_URLS[$i]}" echo "" done echo "View in Azure DevOps:" echo " Boards: https://dev.azure.com/$ORGANIZATION/$PROJECT/_boards" echo " Work Items: https://dev.azure.com/$ORGANIZATION/$PROJECT/_workitems" echo "" # Save mapping SPEC_DIR=$(dirname "$SPEC_FILE") SPECKIT_DIR="$SPEC_DIR/.speckit" mkdir -p "$SPECKIT_DIR" MAPPING_FILE="$SPECKIT_DIR/azure-devops-mapping.json" echo "{" > "$MAPPING_FILE" echo " \"organization\": \"$ORGANIZATION\"," >> "$MAPPING_FILE" echo " \"project\": \"$PROJECT\"," >> "$MAPPING_FILE" echo " \"feature\": \"$FEATURE_NAME\"," >> "$MAPPING_FILE" echo " \"workItems\": [" >> "$MAPPING_FILE" for i in "${!CREATED_IDS[@]}"; do comma="" [[ $i -lt $((${#CREATED_IDS[@]} - 1)) ]] && comma="," echo " {" >> "$MAPPING_FILE" echo " \"storyNumber\": ${SELECTED_STORIES[$i]}," >> "$MAPPING_FILE" echo " \"workItemId\": ${CREATED_IDS[$i]}," >> "$MAPPING_FILE" echo " \"url\": \"${CREATED_URLS[$i]}\"" >> "$MAPPING_FILE" echo " }$comma" >> "$MAPPING_FILE" done echo " ]" >> "$MAPPING_FILE" echo "}" >> "$MAPPING_FILE" echo "Mapping saved: $MAPPING_FILE" fi