Instant CI feedback and Abusing Reflect

A fun little addition I've been added to some of my go projects recently has been a magefile, which in a make-like tool made for go. You make a magefile.go file, add some commands, and execute it like make, e.g mage build; mage run.

One thing I see missing from a lot of makefiles/build scripts that I think is super helpful is feedback on your pipelines. Some services will provide the full build log in your terminal if you push using git hooks, such as dokku, or send an email which is useful but I'd rather not bother with that and stay in my terminal. So I looked up Gitlabs API and it's very simple. After an hour toying around I came up with this:

projects/blog - [master●] » mage status
+------------------------------------------------------+
| master | 2020-08-01T04:43:54.906Z | Build  | running |
| master | 2020-07-30T04:53:08.811Z | Build  | success |
| master | 2020-07-30T03:43:22.924Z | Deploy | success |
| master | 2020-07-30T03:43:22.904Z | Build  | success |
| master | 2020-07-19T01:50:20.206Z | Deploy | success |
+------------------------------------------------------+
projects/blog - [master●] » 

status is a rule defined in my magefile that pulls over every job Gitlab can give me, trims it down to a few (can't do this in the api call), and prints out a small table on the status. There's no fancy lineage yet between jobs/stages, just a flat table with a chronological order. I thought it was pretty neat and saved me some time. It also let's me check an old project if I pick it up again say months lather and want to see when it was last deployed to production successfully.

Bonus content

Since this was easy I decided to have some fun and use golangs reflect package for the first time to see how generic I could make generating that table. Turns out it was pretty easy, and very ugly. Check it out:

// Response of Gitlab jobs API
type Job struct {
  Ref        string `json:"ref"`
  Created_at string `json:"created_at"`
  Name       string `json:"name"`
  Status     string `json:"status"`
}

// Print out latest pipeline statuses
func Status() error {
  jobs := getJobs()
  res := maxes(jobs)

  len_total := 0
  for _, v := range res {
    len_total += v
  }

  // Loop through each job, and then through each field in that job
  println("+---" + strings.Repeat("-", len_total+len(res)) + "----+")
  for x := 0; x < JOB_LIMIT; x++ {
    e := reflect.ValueOf(&jobs[x]).Elem()
    out := ""
    for i := 0; i < e.NumField(); i++ {
      name := e.Type().Field(i).Name
      value := e.Field(i).String()
      if i == e.NumField()-1 {
        out += fmt.Sprintf("| %-*s |", res[name], value)
      } else {
        out += fmt.Sprintf("| %-*s ", res[name], value)
      }
    }
    println(out)
  }
  println("+---" + strings.Repeat("-", len_total+len(res)) + "----+")
  return nil
}

// Dynamically gets all max lengths of a structs fields
func maxes(j []Job) map[string]int {
  m := make(map[string]int)
  for x := range j {
    e := reflect.ValueOf(&j[x]).Elem()
    for i := 0; i < e.NumField(); i++ {
      name := e.Type().Field(i).Name
      value := e.Field(i).String()
      if m[name] == 0 {
        m[name] = len(value)
      }
      if len(value) > m[name] {
        m[name] = len(value)
      }
    }
  }
  return m
}

Wow what a monster. I would never write something like that at my job...

What it does it given an arbitrary struct field, loop through and find the largest string in each, then for each job print out a line padding the field by the largest field. You can add or remove a field, and move them in the type definition to move them around in the final output. This only works if the field type is all the same, or else you need some matching logic. Pretty cool I think but larger and more complex than just hard coding values.