Wednesday, March 23, 2022

Streamlining Trello Task Management from the Command Line

Every few years I tweak my task management strategy. My latest approach leverage's Trello's card & list model. Or, as the industry likes to call it, the Kanban board.

My 'system' works like this: I've got a Trello board with 7 lists: one named On Deck, one for each day of the week minus Saturday.

Every task I need to accomplish is either in the queue (i.e., On Deck) or assigned to a specific day of the week. On Sunday evening, I can plan out my week by moving cards to specific days. Of course, the week never goes as planned and I can adjust cards on the fly.

To pretty things up, each customer gets their own color coded label, which is attached to their cards. Besides giving me a visual cue, the labels let me filter the board to one customer's perspective.

Here's what a board looks like (with the texted blurred out, of course):

Let's Automate This

While this is all working well, I noticed one interaction I could optimize. Nearly all my clients have some task tracking system of their own, so adding tasks to my On Deck list can be automated.

Consider a fake client named FooCo that uses Jira to track their tasks. When I'm assigned the ticket FC-103 to work on, I do the following:

  • Look up the summary of FC-103. For our example, let's assume that the summary is "Implement bitcoin payment method."
  • Create a new card with the title: FC-103: Implement bitcoin payment method.
  • Add the label fooco to the card.
  • Attach the URL https://fooco.atlassian.net/browse/FC-103 to the card.

Given only the bug URL, https://fooco.atlassian.net/browse/FC-103, I can derive the customer, ticket ID, label and ticket summary. The latter value is retrieved using a simple jira command line utility. Once I have the fields, I can use the Trello API to issue a POST to create a new On Deck card.

Here's how this looks in practice:

$ trelloassist -a auto-add -u 'https://fooco.atlassian.net/browse/FC-103'
623b77da1675766212b46454

This process is powered by a shell script named trelloassist. See below for the source code. trelloassist supports querying and creating cards by making simple curl requests to the Trello API.

The magic of the trelloassist's auto-add functionality is the bash function named do_auto_add:

do_auto_add() {
  url=$1 ; shift
  list=<the default destination list>
  case "$url" in
     # for customers who store tasks in Jira
    *.atlassian.net/browse/*)
      ticket=$(basename $url)
      domain=$(echo $url | sed -e 's|https://||'  -e 's|.atlassian.*||')
      title=$(jiraassist -p $domain -a get -k $ticket | cut -d'|' -f4)
      label=$domain
      name="$ticket - $title"
      ;;

     # for customers who store tasks in DoneDone
    *.mydonedone.com/issuetracker/*/issues/*)
      label=scra
      domain=$(echo $url | sed -e 's|https://||'  -e 's|.mydonedone.*||')
      title=$(donedoneassist -a title -i $ticket)
      name="DD:$ticket - $title"
      ;;

    ...

   *)
    echo "Don't know how to auto-add: $url"
    exit 2
    ;;
  esac
}

This function peels off whatever information it can from the URL and then calls an external script (jiraassist and donedoneassist) for additional details. The variables list, label, name and url must be set to sane values when this function completes.

After do_auto_add a call to trelloassist's new-card action is made and the card is added to the board.

    do_auto_add "$url"

    if [ -z "$name" -o -z "$label" ] ; then
      echo "Refusing to auto-add a card without a name or label"
      exit 4
    fi

    trelloassist -a new-card -i "$list" -n "$name" -d "$description" -u "$url" -l "$label"

Thinking of cognitive friction I've smoothed out brings a smile to my face. The Trello API underscores what a flexible tool it is, and why I expect it'll be my Task Manager choice for quite some time.

The Code

Here's the trelloassist script:

#!/bin/bash

profile=i2x

usage() {
  cmd=$(basename $0)
  echo "Usage: $cmd [-p profile] -a boards"
  echo "Usage: $cmd [-p profile] -a lists -i <board-id>"
  echo "Usage: $cmd [-p profile] -a cards -i <list-id>"
  echo "Usage: $cmd [-p profile] -a org -i <id>"
  echo "Usage: $cmd [-p profile] -a mine [-n <name>]"
  echo "Usage: $cmd [-p profile] -a labels -i <board-id>"
  echo "Usage: $cmd [-p profile] -a label-id -n <name>"
  echo "Usage: $cmd [-p profile] -a new-card -i <list-id> -n <name> [ -d <description> ] [ -l <label> ] [ -u <url-attachement> ]"
  echo "Usage: $cmd [-p profile] -a auto-add -u url"
  exit 1
}

api()  {
  method=$1 ; shift
  path=$1 ; shift

  curl -s \
       --request $method \
       --header 'Accept: application/json' \
       "$@" \
       "https://api.trello.com/1/$path?key=$KEY&token=$TOKEN"
}

scrub() {
  if [ "$verbose" = "yes" ] ; then
    jq .
  else
    jq "$@"
  fi
}

named_list() {
  name=$1 ; shift

  for known in $MINE ; do
    known_id=$(echo $known | cut -d ':' -f1)
    known_name=$(echo $known | cut -d ':' -f2)

    if [ "$known_name" = "$name" ] ; then
      echo $known_id
      break
    fi
  done
}

while getopts ":a:b:l:i:n:u:d:hp:v" o; do
  case "$o" in
    n) name=$OPTARG  ;;
    a) action=$OPTARG  ;;
    i) id=$OPTARG  ;;
    l) label=$OPTARG ;;
    d) description=$OPTARG ;;
    u) url=$OPTARG ;;
    v) verbose=yes  ;;
    p) profile=$OPTARG ;;
    *|h) usage  ;;
  esac
done

if [ -f  $HOME/.config/trelloassist/$profile.config ] ; then
  . $HOME/.config/trelloassist/$profile.config
else
  echo "Profile config doesn't exist: $profile"
  exit 3
fi


case "$action" in
  org)
    if [ -z "$id" ] ; then
      usage
    fi

    api GET organizations/$id | scrub '{slug: .name, name: .displayName}'
    ;;

  boards)
    api GET members/$USER/boards | scrub -r '.[] | .id + "|" + .name + "|" + .idOrganization'
    ;;

  lists)
    if [ -z "$id" ] ; then
      usage
    fi

    api GET boards/${id}/lists | scrub -r '.[] | .id + "|" + .name'
    ;;

  cards)
    if [ -z "$id" ] ; then
      usage
    fi

    api GET lists/${id}/cards  | scrub -r '.[] | .id + "|" + .name + "|" + (if (.labels | length) > 0 then .labels | map(.name) | join(",") else "" end) + "|" + .url'
    ;;

  mine)
    for m in $MINE ; do
      id=$(echo $m | cut -d: -f1)
      slug=$(echo $m | cut -d: -f2)
      if [ -z "$name" ] ; then
        echo $slug
      elif [ "$name" = "$slug" ] ; then
        trelloassist -a cards -i $id
      fi
    done
    ;;

  labels)
    if [ -z "$id" ] ; then
      usage
    fi

    api GET "/boards/$id/labels" | scrub -r '.[] | .id + "|" + .name'
    ;;

  label-id)
    if [ -z "$name" ] ; then
      usage
    fi
    trelloassist -a labels -i $DEFAULT_BOARD | grep "$name" | head -1 | cut -d'|' -f1
    ;;

  new-card)
    if [ -z "$id" -o -z "$name" ] ; then
      usage
    fi

    if [ -z "$label" ] ; then
      label_id=""
    else
      label_id=$(trelloassist -a label-id -n "$label")
    fi

    known_id=$(named_list $id)
    if [ -n "$known_id" ] ; then
      id=$known_id
    fi

    id=$(api POST "/cards" \
             --data-urlencode "name=$name" --data-urlencode "desc=$description" \
             -d "pos=top" -d "idLabels=$label_id" \
             -d "idList=$id" | scrub -r '.id')

    if [ -n "$url" ] ; then
      api POST "/cards/$id/attachments" --data-urlencode url="$url" > $HOME/.trelloassist.add.attachment
    fi

    echo $id
    ;;


  auto-add)
    if [ -z "$url" ] ; then
      usage
    fi

    name=""
    list=""
    description=""
    label=""

    do_auto_add "$url"

    if [ -z "$name" -o -z "$label" ] ; then
      echo "Refusing to auto-add a card without a name or label"
      exit 4
    fi

    trelloassist -a new-card -i "$list" -n "$name" -d "$description" -u "$url" -l "$label"

    ;;

  *)
    usage
    ;;
esac

Here's the shape of the profile config file which is found in $HOME/.config/trelloassist/$profile.config:

KEY=[your api key goes here]
TOKEN=[your api token goes here]
USER=[your default user goes here]

DEFAULT_BOARD=[id of your default board]

MINE="
  [list_id]:[list_alias]
  ...
"

do_auto_add() {
 ... see above ...
}

No comments:

Post a Comment