Brain Phrye

code cooking diy fiction personal photos politics reviews tools 


Git Pipeline Status

[ Listen to article ]

At work I tend to pop around a lot of projects fixing (and let’s be honest, breaking) things. And since I spend a lot of time in shell it would be nice if my shell prompt would tell me the status of the pipeline of the project and branch that I’m currently in. Especially when I context switch back to a project, it would be good if cd other/project would tell me not just what branch I was in, but whether it built or didn’t.

Today I poked around and got python-gitlab to do that for me. I use the project’s .git/config to store a cache of pipeline statuses (one per branch) and then emit an emoji to say how things are going. It’s done such that deleting a local branch will also delete the cache for that branch.

The API calls take time which can be annoying as you’re working in a shell. Therefore I tried to limit them with caching. When it’s first run it will cache the project id in pipeline-status.project After that it will use the branch.BRANCH-NAME section to cache the following:

The pipeline-cache-status is only set if it’s been more that 7200 seconds since the last pipeline update. I’ll need to play with that a bit more.

I have some zsh code for printing info about my current branch and so in there I just call this script and put the resulting emoji (if there is one) into there.

Annoyingly I can’t really generalise this script very easily. I could drive it off some settings in ~/.gitconfig but haven’t gotten around to it. For now you’ll have to add the GitLab servers you care about.

And if you use GitHub’s CI system, Travis, CircleCI, Jenkins, AWS’s CI system or any of a host of others, well, sorry, this won’t work.

Adding support for non-gitlab systems would be interesting, but I think before then it might be better to have a little daemon that would capture this - fired off by systemd on Linux systems. The shell could then poke it for info (faster) and it could be smarter about caching and the number of requests to the server (better behaved). Obviously that would involve writing it in a real language!

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env bash

# Documentation:
#
# Need to install python-gitlab and jq.  To configure python-gitlab
# you need a config file named ~/.python-gitlab.cfg with these contents:
#
#   [global]
#   default = site1
#   ssl_verify = true
#   timeout = 5
#
#   [site1]
#   url = https://gitlab.site1.com/
#   private_token = TOKEN-1
#   api_version = 4
#
#   [site2]
#   url = https://gitlab.site2.com/
#   private_token = TOKEN-2
#   api_version = 4
#
# Replace TOKEN-n with your gitlab private tokens - give it the
# read_api and read_user scopes.  The latter might not be needed.

if [[ ! -f ~/.python-gitlab.cfg ]]; then
  exit
fi

export TZ=UTC

url="$(git config \
           remote."$(git for-each-ref --format='%(upstream:remotename)' \
                                      "$(git symbolic-ref -q HEAD)")".url)"

gitlab=(gitlab -o json)

case "$url" in
  https://gitlab.site1.com/*)
    url="${url#https://gitlab.site1.com/}"
    ;;
  git@gitlab.site1.com:*)
    url="${url#git@gitlab.site1.com:}"
    ;;
  https://gitlab.site2.com/*)
    url="${url#https://gitlab.site2.com/}"
    gitlab=(gitlab -o json -g site2)
    ;;
  ssh://git@gitlab.site2.com:2200/*)
    url="${url#ssh://git@gitlab.site2.com:2200/}"
    gitlab=(gitlab -o json -g site2)
    ;;
  *)
    echo "πŸ‘½"
    exit
    ;;
esac

project="$(git config pipeline-status.project)"
if [[ -z "$project" ]]; then
  url="${url%.git}"
  url="${url//\//%2F}"
  project="$("${gitlab[@]}" project get --id "$url" | jq -r '.id')"
  if [[ -z "$project" ]]; then
    echo "πŸ”­"
    exit
  fi
  git config pipeline-status.project "$project"
fi

conf="branch.$(git rev-parse --verify --abbrev-ref HEAD 2> /dev/null)"

hash="$(git rev-parse HEAD)"
cache_hash="$(git config "$conf".pipeline-cache-hash)"
if [[ "$hash" == "$cache_hash" ]]; then
  git config "$conf".pipeline-cache-status && exit
else
  git config "$conf".pipeline-cache-hash "$hash"
  git config --unset "$conf".pipeline-cache-status
  git config --unset "$conf".pipeline-cache-update
fi

cache-update() {
  local update

  update="$("${gitlab[@]}" project-pipeline list \
                             --project-id "$1" --sha "$2" \
            | jq -r '.[] | .updated_at' | sort | tail -1)"
  if [[ -z "$update" ]]; then
    update="$(date +%s)"
  else
    # Won't work on FreeBSD - date does not take a -d flag.
    # Need to parse time in this format: 2020-09-11T10:38:04.394Z
    if [[ "$OSTYPE" == linux-gnu ]]; then
      update="$(date +%s -d "$update")"
    else
      update="$(date -j -f '%FT%H:%M:%S' "${update%.*}" +%s)"
    fi
  fi
  git config "$conf".pipeline-cache-update "$update"
}

cache_update=$(git config "$conf".pipeline-cache-update)
if [[ -z "$cache_update" ]]; then
  cache-update "$project" "$hash"
elif [[ $(( $(date +%s) - cache_update )) -gt 7200 ]]; then
  git config "$conf".pipeline-cache-status && exit
  cache-update "$project" "$hash"
fi
cache_update=$(git config "$conf".pipeline-cache-update)

status="$("${gitlab[@]}" project-pipeline list \
                           --project-id "$project" --sha "$hash" \
          | jq -r '.[] | "\(.updated_at) \(.status)"' \
          | sort | tail -1 | cut '-d ' -f2)"

# NOTE: The pending and skipped emoji do not seem to provide
#       double width hints so don't align properly w/o a training dot.
case "$status" in
  created)              status="πŸŒ…"                         ;;
  waiting_for_resource) status="🚚"                         ;;
  preparing)            status="πŸ—οΈ"                         ;;
  pending)              status="⏸️."                         ;;
  running)              status="πŸš€"                         ;;
  success)              status="πŸŽ‰"                         ;;
  failed)               status="πŸ”₯"                         ;;
  canceled)             status="πŸ›‘"                         ;;
  skipped)              status="⏭️."                         ;;
  manual)               status="πŸ‘‹"                         ;;
  scheduled)            status="⏰"                         ;;
  "")                   status="πŸ’”"                         ;;
  *)                    status="-unknown-status-${status}-" ;;
esac

if [[ $(( $(date +%s) - cache_update )) -gt 7200 ]]; then
  git config "$conf".pipeline-cache-status "$status"
fi

echo "$status"