Buildkite Recent Builds

List all recent builds you can see in Buildkite.

Image preview of Buildkite Recent Builds plugin.

buildkite.1m.sh

Edit
Open on GitHub
#!/usr/bin/env bash

# List recent Buildkite builds
#
# Example output:
#  $ ./bitbar.sh
#  🚀:1
#  ---
#  ✅ Pipeline 1: master [nick@app…]
#  ⏸ Pipeline 2: master [nick@app…]
#  ⏹ Pipeline 2: flaky-test [foo@app…]

# <xbar.title>Buildkite Recent Builds</xbar.title>
# <xbar.version>v0.1</xbar.version>
# <xbar.author>Nicholas Edwards</xbar.author>
# <xbar.author.github>edwardsnjd</xbar.author.github>
# <xbar.desc>List all recent builds you can see in Buildkite.</xbar.desc>
# <xbar.image>https://raw.githubusercontent.com/edwardsnjd/bitbar-buildkite/master/screenshot.gif</xbar.image>
# <xbar.dependencies>bash,curl,jq,buildkite.com</xbar.dependencies>
# <xbar.abouturl>https://github.com/edwardsnjd/bitbar-buildkite/</xbar.abouturl>

# Bash sanity:

set -o errexit   # abort on nonzero exitstatus
set -o errtrace  # abort on error inside any functions or subshells
set -o nounset   # abort on unbound variable
set -o pipefail  # don't hide errors within pipes
# set -o xtrace  # turn on traces, useful while debugging

# Prerequisites:

# Supply your API Token from Buildkite
BUILD_KITE_API_TOKEN="ABC" # <- replace ABC with your token

# Adjust these paths if necessary for your system
CURL="curl"
JQ="/usr/local/bin/jq"

# Configuration:

HOURS_BACK_TO_FETCH=6
ICONS_JSON='{
  "main": "🚀",
  "running": "▶️",
  "passed": "✅",
  "failed": "❌",
  "canceled": "⏹",
  "paused": "⏸"
}'

# Functions:

# -- HTTP with curl --

# fetch_recent_builds_json :: () => Json[]
function fetch_recent_builds_json() {
  local -r since=$(date -u -v-${HOURS_BACK_TO_FETCH}H +"%Y-%m-%dT%H:%MZ")
  local -r url="https://api.buildkite.com/v2/builds?created_from=${since}"
  local -r auth_header="Authorization: Bearer ${BUILD_KITE_API_TOKEN}"

  "${CURL}" --silent -H "${auth_header}" "${url}"
}

# -- JSON transformation with jq --

# run_jq :: Any* -> ?
function run_jq() {
  # Supply common config to all invocations
  "${JQ}" \
    --argjson icons "${ICONS_JSON}" \
    --argjson hours_back "${HOURS_BACK_TO_FETCH}" \
    "$@"
}

# transform_builds :: Json[] -> Json[]
function transform_builds() {
  run_jq -r 'sort_by(.pipeline.name)'
}

# format_active_builds :: Json[] -> String
function format_active_builds() {
  # shellcheck disable=SC2016
  run_jq -r '
    # is_active_job :: Job -> bool
    def is_active_job:
      .state == "running";

    # suffix :: Job[] -> String
    def suffix:
      (map(select(is_active_job)) | length) as $total_active
      | if $total_active > 0 then ":\( $total_active )" else "" end;

    # icon :: String -> String
    def icon(name):
      $icons[name] // name;

    "\( icon("main") ) \( suffix )"
 '
}

# format_builds :: Json[] -> String[]
function format_builds() {
  # shellcheck disable=SC2016
  run_jq -r '
    # is_real_job :: Job -> bool
    def is_real_job:
      .type != "waiter";

    # is_finished_job :: Job -> bool
    def is_finished_job:
      [.state]
      | inside(["passed", "failed", "broken", "timed_out", "canceled" ]);

    # str_trunc(after) :: String -> String
    def str_trunc(after):
      if length > after then "\( .[0:after] )…"
      else . end;

    # icon :: String -> String
    def icon(name):
      $icons[name] // name;

    # build_state_icon :: Job -> String
    def build_state_icon:
      icon(.state);

    # build_job_count :: Job -> String
    def build_job_count:
      .jobs
      | map(select(is_real_job))
      | length as $total
      | ( map(select(is_finished_job)) | length ) as $finished
      | "\($finished)/\($total)";

    # build_creators_by_commit :: Job -> { String: String, ... }
    def build_creators_by_commit:
      map({ key: .commit, value: .creator.email })
      | sort_by(.value)
      | from_entries;

    # color_directive :: Job -> String
    def color_directive:
      if (.state == "running") then "color=white"
      else "" end;

    build_creators_by_commit as $build_creators
    | .[]
    # main row
    | "\( build_state_icon )"
      + " \( .pipeline.name | str_trunc(15) ):"
      + " \( .branch | str_trunc(25) )"
      + " [\( $build_creators[.commit] // "?" | str_trunc(8) )]"
      + "|\( color_directive )"
    # alternate row (shown with Option key)
    , "\( build_state_icon )"
      + " \( .pipeline.name | str_trunc(15) ):"
      + " \( .commit[0:8] )"
      + " [\( build_job_count )]"
      + "|\( color_directive ) alternate=true"
  '
}

# format_blank_slate :: Json[] -> String[]
function format_blank_slate() {
  # shellcheck disable=SC2016
  run_jq -r '
    if length == 0 then
      "No recent builds",
      "No recent builds (in last \( $hours_back ) hours)|alternate=true"
    else "" end
  '
}

# Output lines

# print_bar_row :: () -> String
function print_bar_row() {
  echo "${builds}" | format_active_builds
}

# print_popup_rows :: () -> String[]
function print_popup_rows() {
  echo "${builds}" | format_builds
}

# print_popup_blank_slate :: () -> String[]
function print_popup_blank_slate() {
  echo "${builds}" | format_blank_slate
}

# print_separator :: () -> String
function print_separator() {
  echo "---"
}

# print_instructions :: () -> String[]
function print_instructions() {
  echo "Hold Option key for alternate view"
  echo "Release Option key for default view|alternate=true"
}

# Orchestration:

# main :: () -> String[]
function main() {
  # Get the data
  local -r builds=$(fetch_recent_builds_json | transform_builds)

  # Print bitbar output
  print_bar_row "${builds}"
  print_separator
  print_popup_rows "${builds}"
  print_popup_blank_slate "${builds}"
  print_separator
  print_instructions
}

# Do it!
main