Since a long time ago I’ve been a gitlab-ce user, in fact I’ve set it up on three of the last four companies I’ve worked for (initially I installed it using the omnibus packages on a debian server but on the last two places I moved to the docker based installation, as it is easy to maintain and we don’t need a big installation as the teams using it are small).
On the company I work for now (kyso) we are using it to host all our internal repositories and to do all the CI/CD work (the automatic deployments are triggered by web hooks in some cases, but the rest is all done using gitlab-ci).
The majority of projects are using nodejs as programming
language and we have automated the publication of npm packages on our gitlab
instance npm registry
and even the publication into the npmjs registry.
To publish the packages we have added rules to the gitlab-ci configuration of
the relevant repositories and we publish them when a tag is created.
As the we are lazy by definition, I configured the system to use the tag as
the package version; I tested if the contents of the package.json where in
sync with the expected version and if it was not I updated it and did a force
push of the tag with the updated file using the following code on the script
that publishes the package:
# Update package version & add it to the .build-args
INITIAL_PACKAGE_VERSION="$(npm pkg get version|tr -d '"')"
npm version --allow-same --no-commit-hooks --no-git-tag-version \
"$CI_COMMIT_TAG"
UPDATED_PACKAGE_VERSION="$(npm pkg get version|tr -d '"')"
echo "UPDATED_PACKAGE_VERSION=$UPDATED_PACKAGE_VERSION" >> .build-args
# Update tag if the version was updated or abort
if [ "$INITIAL_PACKAGE_VERSION" != "$UPDATED_PACKAGE_VERSION" ]; then
if [ -n "$CI_GIT_USER" ] && [ -n "$CI_GIT_TOKEN" ]; then
git commit -m "Updated version from tag $CI_COMMIT_TAG" package.json
git tag -f "$CI_COMMIT_TAG" -m "Updated version from tag"
git push -f -o ci.skip origin "$CI_COMMIT_TAG"
else
echo "!!! ERROR !!!"
echo "The updated tag could not be uploaded."
echo "Set CI_GIT_USER and CI_GIT_TOKEN or fix the 'package.json' file"
echo "!!! ERROR !!!"
exit 1
fi
fiThis feels a little dirty (we are leaving commits on the tag but not updating
the original branch); I thought about trying to find the branch using the
tag and update it, but I drop the idea pretty soon as there were multiple
issues to consider (i.e. we can have tags pointing to commits present in
multiple branches and even if it only points to one the tag does not have to
be the HEAD of the branch making the inclusion difficult).
In any case this system was working, so we left it until we started to publish
to the NPM Registry; as we are using a token to push the packages that we
don’t want all developers to have access to (right now it would not matter, but
when the team grows it will) I started to use gitlab
protected
branches on the projects that need it and adjusting the .npmrc file using
protected
variables.
The problem then was that we can no longer do a standard force push for a branch (that is the main point of the protected branches feature) unless we use the gitlab api, so the tags with the wrong version started to fail.
As the way things were being done seemed dirty anyway I thought that the best
way of fixing things was to forbid users to push a tag that includes a
version that does not match the package.json version.
After thinking about it we decided to use
githooks on the
gitlab server for
the repositories that need it, as we are only interested in tags we are going
to use the update hook; it is
executed once for each ref to be updated, and takes three parameters:
- the name of the ref being updated,
- the old object name stored in the ref,
- and the new object name to be stored in the ref.
To install our hook we have found the gitaly relative path of each repo and
located it on the server filesystem (as I said we are using docker and the
gitlab’s data directory is on /srv/gitlab/data, so the path to the repo has
the form /srv/gitlab/data/git-data/repositories/@hashed/xx/yy/hash.git).
Once we have the directory we need to:
- create a
custom_hookssub directory inside it, - add the
updatescript (as we only need one script we used that instead of creating anupdate.ddirectory, the good thing is that this will also work with a standard git server renaming the base directory tohooksinstead ofcustom_hooks), - make it executable, and
- change the directory and file ownership to make sure it can be read and executed from the gitlab container
On a console session:
$ cd /srv/gitlab/data/git-data/repositories/@hashed/xx/yy/hash.git
$ mkdir custom_hooks
$ edit_or_copy custom_hooks/update
$ chmod 0755 custom_hooks/update
$ chown --reference=. -R custom_hooksThe update script we are using is as follows:
#!/bin/sh
set -e
# kyso update hook
#
# Right now it checks version.txt or package.json versions against the tag name
# (it supports a 'v' prefix on the tag)
# Arguments
ref_name="$1"
old_rev="$2"
new_rev="$3"
# Initial test
if [ -z "$ref_name" ] || [ -z "$old_rev" ] || [ -z "$new_rev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# Get the tag short name
tag_name="${ref_name##refs/tags/}"
# Exit if the update is not for a tag
if [ "$tag_name" = "$ref_name" ]; then
exit 0
fi
# Get the null rev value (string of zeros)
zero=$(git hash-object --stdin </dev/null | tr '0-9a-f' '0')
# Get if the tag is new or not
if [ "$old_rev" = "$zero" ]; then
new_tag="true"
else
new_tag="false"
fi
# Get the type of revision:
# - delete: if the new_rev is zero
# - commit: annotated tag
# - tag: un-annotated tag
if [ "$new_rev" = "$zero" ]; then
new_rev_type="delete"
else
new_rev_type="$(git cat-file -t "$new_rev")"
fi
# Exit if we are deleting a tag (nothing to check here)
if [ "$new_rev_type" = "delete" ]; then
exit 0
fi
# Check the version against the tag (supports version.txt & package.json)
if git cat-file -e "$new_rev:version.txt" >/dev/null 2>&1; then
version="$(git cat-file -p "$new_rev:version.txt")"
if [ "$version" = "$tag_name" ] || [ "$version" = "${tag_name#v}" ]; then
exit 0
else
EMSG="tag '$tag_name' and 'version.txt' contents '$version' don't match"
echo "GL-HOOK-ERR: $EMSG"
exit 1
fi
elif git cat-file -e "$new_rev:package.json" >/dev/null 2>&1; then
version="$(
git cat-file -p "$new_rev:package.json" | jsonpath version | tr -d '\[\]"'
)"
if [ "$version" = "$tag_name" ] || [ "$version" = "${tag_name#v}" ]; then
exit 0
else
EMSG="tag '$tag_name' and 'package.json' version '$version' don't match"
echo "GL-HOOK-ERR: $EMSG"
exit 1
fi
else
# No version.txt or package.json file found
exit 0
fiSome comments about it:
- we are only looking for
tags, if theref_namedoes not have the prefixrefs/tags/the script does anexit 0, - although we are checking if the
tagis new or not we are not using the value (ingitlabthat is handled by the protected tag feature), - if we are deleting a
tagthe script does anexit 0, we don’t need to check anything in that case, - we are ignoring if the
tagis annotated or not (we set thenew_rev_typetotagorcommit, but we don’t use the value), - we test first the
version.txtfile and if it does not exist we check thepackage.jsonfile, if it does not exist either we do anexit 0, as there is no version to check against and we allow that on a tag, - we add the
GL-HOOK-ERR:prefix to the messages to show them on the gitlab web interface (can be tested creating a tag from it), - to get the
versionon thepackage.jsonfile we use thejsonpathbinary (it is installed by the jsonpath ruby gem) because it is available on thegitlabcontainer (initially I usedsedto get the value, but a real JSON parser is always a better option).
Once the hook is installed when a user tries to push a tag to a repository
that has a version.txt file or package.json file and the tag does not match
the version (if version.txt is present it takes precedence) the push fails.
If the tag matches or the files are not present the tag is added if the
user has permission to add it in gitlab (our hook is only executed if the
user is allowed to create or update the tag).