1

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]}})
Jared Smith
  • 19,721
  • 5
  • 45
  • 83
  • Macros run at compile time, their purpose being to generate the code that actually gets compiled. Have you tried doing the git-get-sha in a macro, AOT-compiling a call to that macro (i.e., so the code that is finally compiled includes the SHA as a literal string), and including the resulting class file in the uberjar? – Biped Phill Dec 29 '20 at 20:41
  • @BipedPhill I had not, but you are of course correct. If I move away from leiningen which seems to do this automatically, I probably will. – Jared Smith Dec 29 '20 at 20:57

2 Answers2

2

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]))))
jas
  • 10,715
  • 2
  • 30
  • 41
  • I added `clojure.java.io`, copied your function and replaced "your-project" with my project name (I also updated the question with my project.clj), built the jar, and ran it with `java -jar` but `rsrc` seems to be nil: `Exception in thread "main" java.lang.IllegalArgumentException: Cannot open as a Reader.`. Interesting aside: your SO handle is also my initials. – Jared Smith Dec 29 '20 at 18:54
  • Actually, I think you lead me to [what I needed](https://github.com/technomancy/leiningen/blob/master/doc/FAQ.md) I'm experimenting now... – Jared Smith Dec 29 '20 at 19:11
  • That did it, thanks. Now I feel dumb for not reading the FAQ :/ – Jared Smith Dec 29 '20 at 19:36
2

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!).

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48
  • 1
    That would definitely work. But the [leiningen FAQ](https://github.com/technomancy/leiningen/blob/master/doc/FAQ.md) states that since version 2.4.1 they include this information automatically in a file called pom.properties accessible from both the built jar and from `lein run` a la jas' answer. Other than portability (in case I moved to a different build tool) is there any reason not to use it? – Jared Smith Dec 29 '20 at 20:59
  • 1
    It sounds like a nice side benefit of `lein`. I'm not sure if branches etc might complicate the issue. Definitely use a simple script for other situations such as non-git SCM (Mecurial, etc) or where more than the git SHA is needed. For example, you may want to save the build time like '2020-12-29T13:51:43Z', the Clojure version, etc. All of this is easy to add to the `build-info.edn` file. – Alan Thompson Dec 29 '20 at 21:55