
One of the most essential features missing in the free version of GitLab is the ability to vote against resetting the repository by controlling the Merge request (MR) using mandatory code review.
Let's implement the minimum functionality ourselves β we will prohibit Merge until several developers give a 'thumbs up' on the MR.
Why is this even necessary?
Our organization can afford to purchase a GitLab license. However, since development is conducted in a closed environment without internet access, and there is strict budget planning, acquiring self-managed licenses with the necessary functionality may take many months, while we need to start working now.
As a result, we have to:
- either completely prohibit Merges into protected branches for some developers, but then developers with Merge rights face conflicts when merging other people's MRs as a downside;
- or allow uncontrolled merges to your master branch without code review, even if itβs a Junior who just started yesterday.
My first step was to Google it, thinking that surely someone must have already done something similar (without code modifications), but it turned out that such an implementation was not yet available in the community version.
General workflow
As an example, we will set up Merge request approvals on a test repository :
- We will create a token for accessing the GitLab API (through which we will get information about the number of votes 'for' and 'against')
- We will add the token to the GitLab variables
- We will prohibit Merge in case of pipeline errors (if there are not enough 'for' votes)
- We will set up vote checking as part of the CI/CD pipeline
- We will prohibit making commits to protected branches, all changes will be conducted only through MRs
- Let's check what we have achieved in the end
1. Create a token for API access
Go to User Settings β Access Tokens and note the token:

Account for obtaining the token
API access allows you to do almost everything with your repositories, so I advise creating a separate GitLab account, giving it minimal rights to your repositories (for example, Reporter) and obtaining a token for this account.
2. Add the token to GitLab variables
For example, in the previous step we obtained the token QmN2Y0NOUFlfeXhvd21ZS01aQzgK
Open Settings β CI/CD β Variables β Add variable β GITLAB_TOKEN_FOR_CI

In the end, we will get:

This can be done either on a single repository or on a group of repositories.
3. Set a Merge restriction if approvals are not received from colleagues after the code review.
In our case, the Merge restriction will be that the build pipeline returns an error if the required number of votes is not reached.
Go to Settings β General β Merge Requests β Merge Checks and enable the option Build pipelines must succeed.

4. Configure the pipeline
If you haven't set up a CI/CD pipeline for your application yet
Create a file in the root of the repository .gitlab-ci.yml with the simplest content:
stages:
- build
- test
variables:
NEED_VOTES: 1
include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"
run-myapp:
stage: build
script: echo "Hello world"
A separate repository for the CI/CD configuration
I would recommend creating a separate repository where you need to create a myapp.gitlab-ci.yml file for configuring the pipeline. This way, you can better control the access of participants who can change the build pipeline and get an access token.
You will need to specify the location of the new pipeline file by going to the myapp repository β Settings β CI/CD β Build Triggers β Custom CI configuration path β specify the new file, for example myapp.gitlab-ci.yml@gitlab-ce-mr-approvals/Ci
Tip: use a linter to make changes to GitLab CI files
Even if you are working alone, using MR is a good helper, running all your pipeline file changes through the linter. If you make a mistake in the YAML file syntax, it won't break your working pipeline, but will simply block the Merge.
Example of containers with linters that you can integrate into your pipeline:
And an example of a validation stage:
stages:
- lint
lint:
stage: lint
image: sebiwi/gitlab-ci-validate:1.3.0
variables:
GITLAB_HOST: https://gitlab.com
script:
- CI_FILES=(./*.yml)
- for f in "${CI_FILES[@]}"; do
gitlab-ci-validate $f;
done;
Now, you need to add a few parameters to your pipeline to get everything working:
stages:
- test
variables:
NEED_VOTES: 1
include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"
The variable NEED_VOTES determines how many "thumbs up" must be on the MR for the Merge to be available. A value of one means you can approve your own MR by "liking" it.
include adds the test stage that checks the number of "likes".
A Simple Pipeline Example using myapp.gitlab-ci.yml
stages:
- build
- test
variables:
NEED_VOTES: 0
include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"
run-myapp:
stage: build
image: openjdk
script:
- echo CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- java HelloWorld.java
Contents of check-approve.gitlab-ci.yml
ci-mr:
stage: test
script:
- echo ${CI_API_V4_URL}
- echo "CI_PROJECT_ID ${CI_PROJECT_ID}"
- echo "CI_COMMIT_SHA ${CI_COMMIT_SHA}"
- "export MR_ID=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" ${CI_API_V4_URL}\/projects\/${CI_PROJECT_ID}\/merge_requests | jq ".[] | if .sha == \"${CI_COMMIT_SHA}\" then .id else {} end" | grep --invert-match {})"
- "export MR_TITLE=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" ${CI_API_V4_URL}\/projects\/${CI_PROJECT_ID}\/merge_requests | jq ".[] | if .sha == \"${CI_COMMIT_SHA}\" then .title else {} end" | grep --invert-match {})"
- "export MR_WIP=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" ${CI_API_V4_URL}\/projects\/${CI_PROJECT_ID}\/merge_requests | jq ".[] | if .sha == \"${CI_COMMIT_SHA}\" then .work_in_progress else {} end" | grep --invert-match {})"
- "export MR_UPVOTES=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" ${CI_API_V4_URL}\/projects\/${CI_PROJECT_ID}\/merge_requests | jq ".[] | if .sha == \"${CI_COMMIT_SHA}\" then .upvotes else {} end" | grep --invert-match {})"
- "export MR_DOWNVOTES=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" ${CI_API_V4_URL}\/projects\/${CI_PROJECT_ID}\/merge_requests | jq ".[] | if .sha == \"${CI_COMMIT_SHA}\" then .downvotes else {} end" | grep --invert-match {})"
- MR_VOTES=$(expr ${MR_UPVOTES} - ${MR_DOWNVOTES})
- NEED_VOTES_REAL=${NEED_VOTES:-1}
- echo "MR_ID ${MR_ID} MR_TITLE ${MR_TITLE} MR_WIP ${MR_WIP} MR_UPVOTES ${MR_UPVOTES} MR_DOWNVOTES ${MR_DOWNVOTES}"
- echo "MR_VOTES ${MR_VOTES} Up vote = 1, down vote = -1, MR OK if votes >=${NEED_VOTES_REAL}"
- if [ "${MR_VOTES}" -ge "$(expr ${NEED_VOTES_REAL})" ];
then
echo "MR OK";
else
echo "MR ERROR Need more votes";
exit 1;
fi
image: laptevss/gitlab-api-util
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /(^release/.*$)/'
More details on what happens during the review:
- a restriction is set that the review will only occur when creating a MR in the master or release/* branches
- using the GitLab API, we retrieve the number of 'likes' and 'dislikes'
- we calculate the difference between positive and negative feedback
- if the difference is less than the value set in NEED_VOTES, we block the ability to merge
5. Disallow commits to protected branches
We define the branches for which we need to conduct code review and specify that they can only be worked on through MR.
To do this, go to Settings β Repository β Protected Branches:

6. Checking
Let's set NEED_VOTES: 0
We create a MR and put a 'dislike'.

In the build logs:

Now we put a 'like' and initiate a re-check:

Source: habr.com
