How to embed the current git sha in a URL route in Clojure

475 views Asked by At

Typically I have a URL GET route for all of my server-side applications that returns the current git hash as an easy way to check the exact version of the code running on a given instance. In interpreted languages (e.g. Python, Node.js) this is easy, you just check the output of running the shell command in a subprocess. But I'm distributing my Clojure app to instances by packaging it up using lein uberjar.

So I can get the current git sha programmatically using clojure.java.shell like so:

(defn get-git-sha
  [_req]
  (trim ((sh "/bin/sh" "-c" "git rev-parse HEAD") :out)))

(defroutes server-routes
  (GET "/revision" [] get-git-sha))

(defn serve-http
  [port]
  (http-server/run-server server-routes {:port port}))

But I need a way to embed it in the code during the uberjar process (rather than at runtime when the jar is no longer in the repo) to be returned from the URL route I define using compojure and serve via http-kit. How do I run that function at compile time or build time and dump it to a constant or something else I can then return from the route?

While I'd like a solution along those lines, as stated the end game here is to be able to query a running instance via HTTP and find the exact version of the code running (strongly prefer git sha over e.g. a semver number) on a given instance in production.

I realize I can hack my way around this by cloning the repo to all instances and building the jar locally via e.g. ansible and look up the sha in the known directory but that seems, well, hacky, as well as error prone vs "signing" the jar file so to speak at build time.

EDIT:

My project.clj looks like this:

(defproject gps-server "0.1.0-SNAPSHOT"
  :description "Receives GPS data over TCP"
  :url "http://someurl"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [...]
  :main ^:skip-aot gps-server.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot [gps-server.core]}})
2

There are 2 answers

3
jas On BEST ANSWER

I don't think I'm doing anything special to make this happen, so check inside your jar for

 META-INF/maven/your-project/name/pom.properties

Where your-project/name comes from the project.clj you use to build your uberjar

(defproject your-project/name "4.2.6"
  ...

In pom.properties I have:

#Leiningen
#Tue Oct 01 13:20:45 CEST 2019
version=4.2.6
revision=4625a070a34ddc3c563b71025fa5dd907b406331
groupId=your-project
artifactId=name

where the revision is from git.

I have a /version endpoint that returns this information, making use of this function

defn- get-version []
  (let [rsrc (io/resource "META-INF/maven/your-project/name/pom.properties")
        rdr (io/reader rsrc)
        props (java.util.Properties.)]
    (.load props rdr)
    (into {} (for [[k v] props] [k v]))))
2
Alan Thompson On

I had this problem on a project, and they used some crazy undocumented hacks to get the git SHA into a global variable via lein. Don't do that.

Instead, realize that your build process has more than one step. In the simplest case, the two steps are:

  1. Capture the git SHA and save it into a file (typically something like ./resources/build-info.txt (or, even better, build-info.edn).
  2. Invoke lein uberjar to package up everything into a deployment artifact.

So, instead of just calling lein uberjar from the command line, make a 2-line deploy script containing the above steps. Perhaps something as simple as:

#!/bin/bash

# Capture current git SHA (or:  git log -n1 --format=format:"%H")
git rev-parse HEAD  > ./resources/build-info.txt   

# Create uberjar
lein clean ; lein uberjar

# Copy output someplace, etc...

The above could be simply saved into ./scripts/deploy.bash or similar (which, of course, is checked into git!).