Add git commit info to your Elixir Phoenix app

Hi there! We’d like to share how to add git repo information to an Elixir application. In our use case, we wanted to add last commit hash and date to be displayed at Live Qchatex app footer, but it wasn’t as trivial as we expected.

The basic idea was to rely on git console commands to get the desired information:

defp get_commit_sha() do
  System.cmd("git", ["rev-parse", "HEAD"])
  |> elem(0)
  |> String.trim()
end

defp get_commit_date() do
  [sec, tz] =
    System.cmd("git", ~w|log -1 --date=raw --format=%cd|)
    |> elem(0)
    |> String.split(~r/\s+/, trim: true)
    |> Enum.map(&String.to_integer/1)

  DateTime.from_unix!(sec + tz * 36)
end

That’s pretty easy! BUT, only works as long as the .git directory lives within the same directory where your app is started (ie: at runtime).
In cases like building a mix release or deploying the code to gigalixir, things started to get complicated!

Besides not having the .git directory, we didn’t like the idea of calling System.cmd(...) to get the git repo data each time we wanted to display that information at frontend.
Sounds a little expensive and makes not much sense, right?
So, how can we improve this in order to NOT rely on .git directory and to avoid issuing nosense syscalls?

The ‘VERSION’ file

One solution that came to mind was to write the git repo info to a static file and then read it when the app starts. Now the question is: when and where to do such things?

Write the ‘VERSION’ file

One approach is to «override» the mix task mix deps.get and call a function that will get the git repo info and saves it every time the dependencies are updated.
This can be easily done at mix.exs:

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: version(),
      aliases: aliases(),
      ...
    ]
  end

  defp version, do: "1.1.1"

  ...

  defp aliases do
    [
      "deps.get": ["deps.get", &update_version/1]
    ]
  end

  defp update_version(_) do
    contents = [
      version(),
      get_commit_sha(),
      get_commit_date()
    ]

    Mix.shell().info("Updating version with: #{inspect(contents)}")
    File.write("VERSION", Enum.join(contents, "\n"), [:write])
  end

  defp get_commit_sha() do
    System.cmd("git", ["rev-parse", "HEAD"])
    |> elem(0)
    |> String.trim()
  end

  defp get_commit_date() do
    [sec, tz] =
      System.cmd("git", ~w|log -1 --date=raw --format=%cd|)
      |> elem(0)
      |> String.split(~r/\s+/, trim: true)
      |> Enum.map(&String.to_integer/1)

    DateTime.from_unix!(sec + tz * 36)
  end
end

NOTE: Don’t forget to add /VERSION it to .gitignore!

Surelly, that approach can be improved to ensure updating the version every time the app is deployed no matter the «deployment mode» you may choose (ie: running mix release it will NOT update the version, despite we could add that alias to be overriden too..?).
Suggestions are welcome! =]

Read the ‘VERSION’ file

Again, one approach is to read and parse the VERSION file contents at application start.
To do that, we add some lines to application.ex file:

defmodule MyApp.Application do
  use Application
  require Logger

  def start(_type, _args) do
    # Load application version
    load_version()
    ...
  end

  def version, do: Application.get_env(:my_app, :version)
  def version(key), do: version()[key]

  defp load_version() do
    [vsn, hash, date] =
      case File.read("VERSION") do
        {:ok, data} -> data |> String.split("\n")
        _ -> [nil, nil, nil]
      end

    version = %{vsn: vsn, hash: hash, date: date}
    Logger.info("Loading app version: #{inspect(version)}")
    Application.put_env(:my_app, :version, version)
  end
end

That way we archieved to load the version info and to set it at application env scope by doing:
Application.put_env(:my_app, :version, version)

Now, to retrieve the version values at runtime, we could directly call:
Application.get_env(:my_app, :version)
But we added a little helper to abstract it a little:
MyApp.Application.version()

Displaying the version info in a Phoenix View

We could add some code to the layout_view.ex in order to easily display the current version info in a view:

defmodule MyApp.LayoutView do
  use MyAppWeb, :view

  def render_version(),
    do: "MyApp <b>v#{get_app_version()}.#{get_commit_date()}</b> at #{get_commit_link()}"

  def repo_url(path \\ ""),
    do: Application.get_env(:my_app, MyAppWeb.Endpoint)[:repo] <> path

  defp get_app_version(), do: Application.spec(:my_app, :vsn)
  defp get_commit_sha(), do: MyApp.Application.version(:hash)
  defp get_commit_date(), do: MyApp.Application.version(:date)

  defp get_commit_link() do
    commit_sha = get_commit_sha()
    path = "/commit/#{commit_sha}"

    "<a href=\"#{repo_url(path)}\" target=\"_blank\">##{commit_sha}</a>"
  end
end

NOTE: The function get_commit_link() calls repo_url(path) that gets the repo url from a custom config param manually set:
Application.get_env(:my_app, MyAppWeb.Endpoint)[:repo]
By having defined at config.exs:

config :my_app, MyAppWeb.Endpoint,
  environment: Mix.env(),
  repo: "https://github.com/my_repo"

And lastly, in a view template like app.html.eex just add:

<footer><%= raw render_version() %></footer>

..and voilà!

It should print something like:
MyApp v0.1.1.2019-07-14 17:12:51Z at #c76563d34...

Hope it helped someone and please feel free to post your comments and suggestions! =]

2019-07-15

Diego Calero

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *