GitHub Actions and vanity metrics

The measure of a modern software engineer in our attention economy is easily calculable thanks to companies like Microsoft acting both as bastion and companion in demonstrating one's prowess.

The quality of the modern software craftsperson can be calculated using the following formula:

Q(stars+downloads)slocQ \propto \frac{(stars + downloads)}{sloc}

What this shows is we need to maximise engagement relative to the amount of code written in order to be recognised as a superstar coder, the fabled 10x developer.

By maximising our quality quotient, we ensure a guaranteed place at the next venture-backed unicorn, where one’s fraction of a point will yield at least double your annual salary in just 10+ years of daily standups, years waiting on CI, a couple of paltry promotions and a minimum four rounds of dilution.

Taste is everything

As we all know, code is a liability that brings with it tedious tests and troublesome tooling. Minor discomforts that ensure the introduction of microservices, event sourcing, and ultimately a large rewrite.

As connoisseurs, we’ll elect for an elegant and succinct language to keep SLOC to a minimum, without having to resort to golfing.

Any language that insists on automated, invisible insertion of semicolons, or that promotes verbosity is immediately out.

APL-family languages rank highly on both expressivity and power, but no one’s going to believe your hexadecimal-to-RGB colour conversion library written in Q has legitimately been downloaded a thousand times this week.

The obvious choice is a Lisp, and to firmly plant a foot in both the highly academic, left-leaning libertarian camp and hyper-corporate, capitalist, FTSE-ranking enterprise, we’ll be going with a Lisp that runs on your favourite flavours of both Java and JavaScript Virtual Machines. That’s right, the equally hip and stable, Clojure.

Silicon minions

While the days of free compute subsidised by venture capital are largely behind us, Microsoft do still give away a few minutes of compute each month to all GitHub users, so it’s possible for us to demonstrate our technical chops by infating those all important numbers with a little bit of automation. 🐣🐣🪨

If you don’t already have a couple of small packages that do one thing well, you might fork an established project, rebrand it, and improve it by adding a code of conduct or helpfully reformatting the ugly source code using Prettier. The Clojure codebase itself could be a good starting point for taking your career to the moon.

I’ll use a couple of small libraries I wrote some years ago to propel my rocketship.

  • invetica/media-types
  • invetica/spec
  • invetica/uri

Our language of choice relies on a package repository owned by Microsoft GitHub. Clojars provides us with handy little badges we can embed in our webpages but these don’t include the all important counters, so we’ll have to team up with Shields.io and relax our CSP accordingly.

invetica/media-types downloadsinvetica/spec downloadsinvetica/uri downloads

We’ll be making use of GitHub’s scheduled workflows to fetch our unduly ignored packages on the hour. You could choose to do so more frequently, or on a more arcane schedule but who’s got time for cron syntax when you can just sleep.

As this code executes on the backend rather than in the browser, it is written in YAML.

on:
  workflow_dispatch:

  schedule:
    - cron: '0 * * * *'

jobs:
  prepare:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Prepare java
        uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: '8'

      - name: Install clojure tools
        uses: DeLaGuardo/setup-clojure@11.0
        with:
          cli: latest

      - name: Deep breath
        run: bin/delay

      - name: Prepare a selection of delightful dependencies
        run: bin/prepare

At this point you’re probably wondering why my steps are so poetically named. Do not forget that code is poetry, and that markup languages like YAML inevitably evolve into Turing-complete interpolated programming environments. Leverage enjambment to pay homage to punch cards.

To fly under the radar of other engineers wary of our meteoric rise, we instruct our silicon slave to take a deep breath before tickling a fellow automaton.

#!/usr/bin/env bash
interval=`shuf -i 1-60 -n 1`
echo >&2 "==> Waiting $interval seconds..."
sleep "$interval"

Notice the artisinal use of stderr, combined with adherance to aesthetic tradition in the use of a fat arrow ahead of any annoucement.

The final step in our workflow invokes a mighty virtual machine to execute the most elegant of all programming paradigms: Java Lisp.

#!/usr/bin/env bash
exec clojure -M --report stderr --main vanity.main deps.edn

For anyone unfamiliar with Clojure, things are about to get really weird.

Teaching the cow to sing

We begin by defining a set of dependencies using aliases. Aliases allow us to modify the paths used by our mighty virtual machine; paths that include our own source code, third-party dependencies and most importantly first party.

With this separation, we can start nested baby JVMs that conditionally pull additional (or extra in tools.deps parlance) dependencies at our behest. One could, of course, trivially pull the full set of awe-inspiring artefacts, but over-engineering a solution is half the fun.

{:paths ["src"]

 :aliases
 {:invetica/media-types
  {:extra-deps {invetica/media-types {:mvn/version "RELEASE"}}}

  :invetica/spec
  {:extra-deps {invetica/spec {:mvn/version "RELEASE"}}}

  :invetica/uri
  {:extra-deps {invetica/uri {:mvn/version "RELEASE"}}}}}

I’m only interested in appearing popular now. What you do in the past is irrelevant in the attention economy, unless you insult a protected group. This justifies my dubious use of "RELEASE" versions to always have the latest, greatest bits fetched on the hour.

With our focus on the immediate, and our set of dependencies neatly compartmentalised by namespace-qualified keyword, it’s time to carefully compose a rigid hymn for a random selection of water-cooled cattle to execute unquestioningly. Suprisingly toasty, the chosen subject will forever more sing our digital praises, guaranteeing a position atop one of the flock of AI unicorns currently grazing their way down Sand Hill Road.

Cyclometric iambic pentameter

Having spent at least 15 minutes on Learn You a Haskell, one knows best practice is to separate purity from icky IO. We apply this same wisdom to our vanity engine, and begin by extracting the set of keys from our :aliases. An operation we can push to the periphery of our thoroughly over-complicated creation.

(defn read-aliases
  [path]
  (into [] (-> path slurp edn/read-string :aliases keys)))

The keen-eyed reader might be wondering why we produce a vector rather than a set. “Duplicate keys in a map will only cause confusion” they yell, holding back the smug grin that would reveal the shallow ego underpinning an endless need to belittle others with their knowledge.

Our execution time will barely register relative to the immense task of indexing docstrings across thousands of namespaces or the absurd amount of work we’re doing over networks shifting bytes. And perhaps we’ll need indexed access to shuffle things around later. As I always like to say, we might gonna need it.

So with our aliases in hand, we can now once again choose to elude engineers (of a similar calibre) who may attempt to thwart our progress by introducing a little randomness.

(defn some-random-shuffle
  ([aliases]
   (some-random-shuffle aliases 0.5))
  ([aliases prob]
   (->> #(random-sample prob aliases)
        repeatedly
        (filter seq)
        first)))

The statiticians among us will point out that over a long enough period of time, the random sample will be entirely redundant, but the use of threading lazy-sequence operations together is too good to pass up. We do pause, however, to pat ourselves on the back for resisting the temptation to introduce an explicit transduction.

More than just heating the room

Finally, we reach our main :: IO! Now we can plumb together the pure and impure in a beautifully testable cacophony of bytecodes and CPU instructions.

(defn -main
  [path]
  (let [aliases (-> path read-aliases some-random-shuffle)
        cmd     (prepare-cmd aliases)
        result  (apply sh/sh cmd)]
    (pprint {:aliases aliases
             :cmd     cmd
             :result  result})
    (System/exit (:exit result))))

A proud, parent JVM spawning a brand new baby JVM. New life — two generations of virtual minions birthed into this world, short-lived, with the sole purpose of satisfying the unending human need for recognition. We have done something wonderful here today.

And with that, we refresh the all mighty page, only to see the same number as before because someone decided to cache the counters. We must wait until tomorrow to see 319 become 405.

Gimme kiss 😘

In our pursuit of professional recognition we’ve had to familiarise ourselves with GitHub workflows, some POSIX shell, a dash of Clojure, the JVM and its classpath, and to really spice things up, I threw in some Emacs, Org-mode, Docker and Nix.

alias curiosity=kill
cat & ; curiosity $!

If you’re in a hurry and want to keep things simple, you can accomplish the same feat with this single invocation of clojure:

clojure -P -Sdeps \
'{:deps
  {invetica/media-types {:mvn/version "RELEASE"}
   invetica/spec        {:mvn/version "RELEASE"}
   invetica/uri         {:mvn/version "RELEASE"}}}'

But where’s the fun in that‽

For a working example that’s no longer scheduled to ensure my success, visit Microsoft forge.