Git status for Elixir builds

This is a quick tale of debugging a production issue and a nice solution discovered along the way. Whilst investigating the bug it was not clear what version of the code was running. What was needed was a quick way to associate running code with a git commit.

Fetching a Git commit

A quick search uncovered this stack overflow answer. With a bit of work to handle the error case we ended up with this code to get the current commit hash.


      def commit() do
        case System.cmd("git", ["rev-parse", "HEAD"]) do
          {hash, 0} ->
            String.trim(hash)
          _ ->
            "UNKNOWN"
        end
      end
    

Compile time vs Run time

This code will get the commit information from the system when it is run. What was needed was the commit information from when the code was compiled.

Using exactly the same code but this time outside a function will fetch the git commit at compile time. This value can then be exposed in a function that references the compile time value.


      @commit (case System.cmd("git", ["rev-parse", "HEAD"]) do
        {hash, 0} ->
          String.trim(hash)
        _ ->
          Logger.warn("Could not read git commit hash")
          "UNKNOWN"
      end)

      def commit(), do: @commit
    

GitStatus

The solution is wrapped up in a compact hex package git_status.

Clean code only, please

The project also defines is_clean? to check if the git project had any uncommited content at compilation. This can be used to create a guard test for CI ensuring that a build is always associated with the code in a commit.


      @tag :ci
      test "is clean" do
        assert GitStatus.is_clean?()
      end
    

If practising TDD locally you can add the :ci tag to excluded tests.

Raxx integration

Our project is a web API that already has endpoints under /sys with metadata on the service. All that was required was a simple web action for a /sys/source endpoint.


      defmodule MyApp.API.SourceStatus do
        use Raxx.Server

        @response response(:ok)
        |> set_header("content-type", "application/json")
        |> set_body(Poison.encode!(%{
          commit: GitStatus.commit(),
          clean: GitStatus.is_clean?()
        })

        def handle_request(
          %{method: :GET, path: ["sys", "source"]},
          _state)
        do
          @response
        end
      end