|
| 1 | +#!/usr/bin/env bash |
| 2 | +# asana-create-dep-task.sh |
| 3 | +# Create a dependent Asana task that blocks a parent task. |
| 4 | +# Checks for existing dependencies first to avoid duplicates. |
| 5 | +# |
| 6 | +# Usage: |
| 7 | +# asana-create-dep-task.sh --parent <parent_gid> --name "task name" [--notes "description"] [--assignee <user_gid>] |
| 8 | +# |
| 9 | +# If --assignee is omitted, the task is assigned to the current user |
| 10 | +# (resolved via asana-whoami.sh). |
| 11 | +# |
| 12 | +# Requires env var: ASANA_TOKEN |
| 13 | +# |
| 14 | +# Output: |
| 15 | +# TASK_GID: <gid> |
| 16 | +# TASK_URL: <url> |
| 17 | +# CREATED: true|false (false if task already existed) |
| 18 | +# ASSIGNED_TO: <user_gid> |
| 19 | +# FIELDS_SET: priority=<val>, status=<val> |
| 20 | +# DEPENDENCY_SET: <new_gid> blocks <parent_gid> |
| 21 | +# |
| 22 | +# Exit codes: 0 = success, 1 = error |
| 23 | +set -euo pipefail |
| 24 | + |
| 25 | +PARENT_GID="" |
| 26 | +TASK_NAME="" |
| 27 | +TASK_NOTES="" |
| 28 | +ASSIGNEE_GID="" |
| 29 | + |
| 30 | +while [[ $# -gt 0 ]]; do |
| 31 | + case "$1" in |
| 32 | + --parent) PARENT_GID="$2"; shift 2 ;; |
| 33 | + --name) TASK_NAME="$2"; shift 2 ;; |
| 34 | + --notes) TASK_NOTES="$2"; shift 2 ;; |
| 35 | + --assignee) ASSIGNEE_GID="$2"; shift 2 ;; |
| 36 | + *) echo "Unknown flag: $1" >&2; exit 1 ;; |
| 37 | + esac |
| 38 | +done |
| 39 | + |
| 40 | +if [[ -z "$PARENT_GID" || -z "$TASK_NAME" ]]; then |
| 41 | + echo "Usage: asana-create-dep-task.sh --parent <gid> --name <name> [--notes <desc>] [--assignee <gid>]" >&2 |
| 42 | + exit 1 |
| 43 | +fi |
| 44 | + |
| 45 | +if [[ -z "${ASANA_TOKEN:-}" ]]; then |
| 46 | + echo "Error: ASANA_TOKEN not set" >&2 |
| 47 | + exit 1 |
| 48 | +fi |
| 49 | + |
| 50 | +API="https://app.asana.com/api/1.0" |
| 51 | +AUTH="Authorization: Bearer $ASANA_TOKEN" |
| 52 | + |
| 53 | +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" |
| 54 | + |
| 55 | +# Auto-resolve assignee to current user if not provided |
| 56 | +if [[ -z "$ASSIGNEE_GID" ]]; then |
| 57 | + ASSIGNEE_GID=$("$SCRIPT_DIR/asana-whoami.sh" 2>/dev/null || true) |
| 58 | +fi |
| 59 | + |
| 60 | +# Phase 1: Check if a dependency with a matching name already exists |
| 61 | +existing=$(curl -s "$API/tasks/$PARENT_GID/dependencies?opt_fields=name&limit=100" \ |
| 62 | + -H "$AUTH" | python3 -c " |
| 63 | +import sys, json |
| 64 | +data = json.load(sys.stdin).get('data', []) |
| 65 | +target = '''$TASK_NAME''' |
| 66 | +for dep in data: |
| 67 | + if dep.get('name', '').strip().lower() == target.strip().lower(): |
| 68 | + print(dep['gid']) |
| 69 | + sys.exit(0) |
| 70 | +print('') |
| 71 | +") |
| 72 | + |
| 73 | +if [[ -n "$existing" ]]; then |
| 74 | + echo "TASK_GID: $existing" |
| 75 | + echo "TASK_URL: https://app.asana.com/0/0/$existing" |
| 76 | + echo "CREATED: false" |
| 77 | + exit 0 |
| 78 | +fi |
| 79 | + |
| 80 | +# Phase 2: Get parent task's project and custom fields to copy |
| 81 | +parent_info=$(curl -s "$API/tasks/$PARENT_GID?opt_fields=workspace.gid,memberships.project.gid,memberships.project.name,custom_fields.gid,custom_fields.enum_value.gid,custom_fields.enum_value.name" \ |
| 82 | + -H "$AUTH") |
| 83 | + |
| 84 | +read -r WORKSPACE_GID PROJECT_GIDS PRIORITY_GID PRIORITY_VAL STATUS_GID STATUS_VAL < <(echo "$parent_info" | python3 -c " |
| 85 | +import sys, json, re |
| 86 | +data = json.load(sys.stdin)['data'] |
| 87 | +ws = data.get('workspace', {}).get('gid', '') |
| 88 | +
|
| 89 | +# Collect all non-version projects (board/backlog projects, not release milestones) |
| 90 | +projects = [] |
| 91 | +for m in data.get('memberships', []): |
| 92 | + p = m.get('project', {}) |
| 93 | + if not re.match(r'^\d+\.\d+\.\d+$', p.get('name', '')): |
| 94 | + projects.append(p.get('gid', '')) |
| 95 | +if not projects and data.get('memberships'): |
| 96 | + projects.append(data['memberships'][0]['project']['gid']) |
| 97 | +proj_str = ','.join(projects) |
| 98 | +
|
| 99 | +# Custom fields to copy |
| 100 | +FIELD_MAP = { |
| 101 | + '795866930204488': 'priority', |
| 102 | + '1190660107346181': 'status', |
| 103 | +} |
| 104 | +fields = {} |
| 105 | +for f in data.get('custom_fields', []): |
| 106 | + label = FIELD_MAP.get(f['gid']) |
| 107 | + if label and f.get('enum_value'): |
| 108 | + fields[label + '_gid'] = f['gid'] |
| 109 | + fields[label + '_val'] = f['enum_value']['gid'] |
| 110 | + fields[label + '_name'] = f['enum_value']['name'] |
| 111 | +
|
| 112 | +pri_gid = fields.get('priority_gid', '') |
| 113 | +pri_val = fields.get('priority_val', '') |
| 114 | +sta_gid = fields.get('status_gid', '') |
| 115 | +sta_val = fields.get('status_val', '') |
| 116 | +
|
| 117 | +print(f'{ws} {proj_str} {pri_gid}:{pri_val}:{fields.get(\"priority_name\",\"\")} {sta_gid}:{sta_val}:{fields.get(\"status_name\",\"\")}') |
| 118 | +") |
| 119 | + |
| 120 | +PRIORITY_FIELD=$(echo "$PRIORITY_VAL" | cut -d: -f1) |
| 121 | +PRIORITY_ENUM=$(echo "$PRIORITY_VAL" | cut -d: -f2) |
| 122 | +PRIORITY_NAME=$(echo "$PRIORITY_VAL" | cut -d: -f3) |
| 123 | +STATUS_FIELD=$(echo "$STATUS_VAL" | cut -d: -f1) |
| 124 | +STATUS_ENUM=$(echo "$STATUS_VAL" | cut -d: -f2) |
| 125 | +STATUS_NAME=$(echo "$STATUS_VAL" | cut -d: -f3) |
| 126 | + |
| 127 | +# Phase 3: Create the task |
| 128 | +NOTES_JSON=$(python3 -c "import json; print(json.dumps('''$TASK_NOTES'''))") |
| 129 | + |
| 130 | +custom_fields_json="{}" |
| 131 | +if [[ -n "$PRIORITY_FIELD" && -n "$PRIORITY_ENUM" ]]; then |
| 132 | + custom_fields_json=$(python3 -c " |
| 133 | +import json |
| 134 | +cf = {} |
| 135 | +pf = '$PRIORITY_FIELD' |
| 136 | +pe = '$PRIORITY_ENUM' |
| 137 | +sf = '$STATUS_FIELD' |
| 138 | +se = '$STATUS_ENUM' |
| 139 | +if pf and pe: cf[pf] = pe |
| 140 | +if sf and se: cf[sf] = se |
| 141 | +print(json.dumps(cf)) |
| 142 | +") |
| 143 | +fi |
| 144 | + |
| 145 | +# Build projects list from comma-separated GIDs |
| 146 | +IFS=',' read -ra PROJECT_ARR <<< "$PROJECT_GIDS" |
| 147 | + |
| 148 | +new_task=$(curl -s "$API/tasks" \ |
| 149 | + -H "$AUTH" \ |
| 150 | + -H "Content-Type: application/json" \ |
| 151 | + -d "$(python3 -c " |
| 152 | +import json |
| 153 | +projects = '''$PROJECT_GIDS'''.split(',') |
| 154 | +assignee = '''$ASSIGNEE_GID''' or None |
| 155 | +data = { |
| 156 | + 'data': { |
| 157 | + 'name': '''$TASK_NAME''', |
| 158 | + 'notes': $NOTES_JSON, |
| 159 | + 'projects': [p for p in projects if p], |
| 160 | + 'workspace': '$WORKSPACE_GID', |
| 161 | + 'custom_fields': $custom_fields_json |
| 162 | + } |
| 163 | +} |
| 164 | +if assignee: |
| 165 | + data['data']['assignee'] = assignee |
| 166 | +print(json.dumps(data)) |
| 167 | +")") |
| 168 | + |
| 169 | +NEW_GID=$(echo "$new_task" | python3 -c " |
| 170 | +import sys, json |
| 171 | +data = json.load(sys.stdin) |
| 172 | +if 'errors' in data: |
| 173 | + print('ERROR: ' + json.dumps(data['errors']), file=sys.stderr) |
| 174 | + sys.exit(1) |
| 175 | +print(data['data']['gid']) |
| 176 | +") |
| 177 | + |
| 178 | +if [[ -z "$NEW_GID" || "$NEW_GID" == "ERROR"* ]]; then |
| 179 | + echo "Error creating task" >&2 |
| 180 | + exit 1 |
| 181 | +fi |
| 182 | + |
| 183 | +FIRST_PROJECT=$(echo "$PROJECT_GIDS" | cut -d, -f1) |
| 184 | +echo "TASK_GID: $NEW_GID" |
| 185 | +echo "TASK_URL: https://app.asana.com/0/$FIRST_PROJECT/$NEW_GID" |
| 186 | +echo "CREATED: true" |
| 187 | +[[ -n "$ASSIGNEE_GID" ]] && echo "ASSIGNED_TO: $ASSIGNEE_GID" |
| 188 | + |
| 189 | +# Phase 4: Set as blocking dependency |
| 190 | +curl -s -X POST "$API/tasks/$PARENT_GID/addDependencies" \ |
| 191 | + -H "$AUTH" \ |
| 192 | + -H "Content-Type: application/json" \ |
| 193 | + -d "{\"data\": {\"dependencies\": [\"$NEW_GID\"]}}" > /dev/null |
| 194 | + |
| 195 | +echo "DEPENDENCY_SET: $NEW_GID blocks $PARENT_GID" |
| 196 | + |
| 197 | +if [[ -n "$PRIORITY_NAME" || -n "$STATUS_NAME" ]]; then |
| 198 | + fields_msg="" |
| 199 | + [[ -n "$PRIORITY_NAME" ]] && fields_msg="priority=$PRIORITY_NAME" |
| 200 | + [[ -n "$STATUS_NAME" ]] && fields_msg="${fields_msg:+$fields_msg, }status=$STATUS_NAME" |
| 201 | + echo "FIELDS_SET: $fields_msg" |
| 202 | +fi |
0 commit comments