Sam Hooke

GitLab CI and poetry-dynamic-versioning

tl;dr: The branch variable in poetry-dynamic-versioning does not work if your Git repository has a detached HEAD, which is likely to be the case under GitLab. Fortunately, there is an easy fix.

The poetry-dynamic-versioning plugin provides a branch variable which is the name of the current branch, or if that fails, None:

branch (string or None)

Assuming branch is not None, this allows you to change the build version depending upon the branch. For example to have untagged builds on the main branch be alpha, and all other branches be dev:

format-jinja = """
    {%- if distance == 0 -%}
        {{- base -}}
    {%- elif branch == "main" -%}
        {{- bump_version(base) }}.alpha{{ distance }}+g{{commit}}
    {%- else -%}
        {{- base }}.dev{{ distance }}+g{{commit}}
    {%- endif -%}
"""

This works locally, but unfortunately under GitLab CI the branch variable returns None, preventing it from working.

Verifying branch is None under GitLab CI §

You can verify this with the following:

format-jinja = """
    TEST_{{- branch -}}_TEST
"""

This will fail locally and on GitLab CI, because TEST_<blah>_TEST is not a valid Python version. But crucially, it will fail differently!

Assuming you are running it on the branch main:

  • Locally it will fail with TEST_main_TEST.
  • On GitLab CI it will fail with TEST_None_TEST.

Investigating why we get None under GitLab CI §

Looking at the plugin source, the plugin gets the Version object from dunamai. Following through in the dunamai source, it gets the branch variable from Git:

code, msg = _run_cmd("git symbolic-ref --short HEAD", path, codes=[0, 128])
if code == 128:
    branch = None
else:
    branch = msg

If we try this out manually in GitLab CI, we see the issue:

$ git symbolic-ref --short HEAD
fatal: ref HEAD is not a symbolic ref

The Git command is failing, which explains why branch is defaulting to None under GitLab CI.

It fails because Git is checking out a commit, rather than a branch, and so it has a “detached head”.

For example, see this GitLab tutorial:

Checking out 7226fc70 as detached HEAD (ref is main)...

Fixing the detached head in GitLab §

This can be fixed by calling the following in GitLab CI:

git checkout "$CI_COMMIT_REF_NAME"

Call it in before_script (or wherever makes more sense), and it will ensure the checkout does not have a detached HEAD. This will allow git symbolic-ref --short HEAD to get the branch name, and in poetry-dynamic-verisoning, the branch variable will be populated correctly.

Appendix §

Attempted fix with GIT_DEPTH: 0 §

The Dunamai README warns that it needs access to the full version history, and suggests using GIT_DEPTH: 0 in GitLab. However, in my testing this didn’t seem to address the issue of Git performing a detched HEAD clone. For example, with the following in .gitlab-ci.yml:

variables:
    GIT_DEPTH: 0
    CI_DEBUG_TRACE: "true"

A detached HEAD clone still occurs in the GitLab job log:

Created fresh repository.
Checking out ac3b2af1 as test-git-depth...
++ echo 'Checking out ac3b2af1 as test-git-depth...'
++ git checkout -f -q ac3b2af1ac7c21afe393ab1238ec35cc8ffcd48c

A potential better fix? §

It may also be possible to avoid the deatched HEAD situation by changing your GitLab workflow(?). Further reading and investigation required.

These are rough notes that vary greatly in quality and length, but prove useful to me, and hopefully to you too!

← Previous: How to set up Ansible and Vagrant in WSL
Next: Reflections on 10 years as a professional Software Engineer →