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
fi
This 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_hooks
sub directory inside it, - add the
update
script (as we only need one script we used that instead of creating anupdate.d
directory, the good thing is that this will also work with a standard git server renaming the base directory tohooks
instead 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_hooks
The 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
fi
Some comments about it:
- we are only looking for
tags
, if theref_name
does not have the prefixrefs/tags/
the script does anexit 0
, - although we are checking if the
tag
is new or not we are not using the value (ingitlab
that is handled by the protected tag feature), - if we are deleting a
tag
the script does anexit 0
, we don’t need to check anything in that case, - we are ignoring if the
tag
is annotated or not (we set thenew_rev_type
totag
orcommit
, but we don’t use the value), - we test first the
version.txt
file and if it does not exist we check thepackage.json
file, 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
version
on thepackage.json
file we use thejsonpath
binary (it is installed by the jsonpath ruby gem) because it is available on thegitlab
container (initially I usedsed
to 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
).