There is a way to only run a job (or jobs) when a merge request is approved, but it is more complicated since you have to interact with the Approvals API. However, access to the Approvals API is only available for paying customers, either on gitlab.com or a self-hosted Gitlab instance.
The Approvals API has an operation that gets the approval state of a Merge Request (documentation is here: https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-the-approval-state-of-merge-requests). You can call it with: curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://your.gitlab.instance.com/api/v4/projects/:project_id:/merge_requests/:merge_request_id:/approval_state"
where $PRIVATE_TOKEN
is a personal access token with at least the api
scope.
The :project_id
field is the ID of your project, which you can get from one of the predefined variables Gitlab CI provides all jobs: $CI_PROJECT_ID
. The :merge_request_id:
is the ID of the specific Merge Request for the pipeline, if there is one: $CI_MERGE_REQUEST_IID
. With these two variables, the curl command is now: curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://your.gitlab.instance.com/api/v4/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/approval_state". Note: there are two predefined variables for merge request ID's. One is
$CI_MERGE_REQUEST_ID, which is the gitlab-instance-wide ID, and the other is
$CI_MERGE_REQUEST_IID` which is the project-specific ID. For this operation, we need the project-specific IID variable.
The result of the "Get Approval State" operation has information like the number of required approvers (if applicable), the eligible approvers, and who has approved thus far. It looks like this:
{
"approval_rules_overwritten": true,
"rules": [
{
"id": 1,
"name": "Ruby",
"rule_type": "regular",
"eligible_approvers": [
{
"id": 4,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"approvals_required": 2,
"users": [
{
"id": 4,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"groups": [],
"contains_hidden_groups": false,
"approved_by": [
{
"id": 4,
"name": "John Doe",
"username": "jdoe",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
"web_url": "http://localhost/jdoe"
}
],
"source_rule": null,
"approved": true,
"overridden": false
}
]
}
Once you have the result, you'll have to parse it so you can use the information to determine what jobs will run or not. jq
is a good option. First, save the output from the Approvals API to a file, then we can use jq
. If required approvals is enabled for your Merge Requests, you can get the number required with cat output | jq '.["rules"][]["approvals_required"]'
. To get the number of people who have approved the Merge Request, we can parse the file with: cat output | jq '.["rules"][]["approved_by"] | length'
You can read more about jq
in the manual.
Once you have those values, it's up to you to decide when a Merge Request is considered "approved" or not. Maybe you need to have all required approvals, or just one approver, or an approval by a specific person.
Depending on the number of jobs you want to only run for approved merge requests, you can either run all of this in the job itself, but if you have many jobs that shouldn't run unless approved, this can be frustrating to put in every job. Fortunately, there's another Gitlab CI feature that can help us. As of Gitlab version 12.9, you can upload a file as an artifact
and it will treat the contents as environment variables for later stages in your pipeline.
As an example, let's add a job that runs before all other jobs in our pipeline to hit the Approvals API, use jq
to parse the output, and decide if our jobs should run or not.
stages:
- check_approvals
...
Check Merge Request Approvals:
stage: check_approvals
when:never
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
when: always
script:
- curl --header "PRIVATE-TOKEN: ${API_ACCESS_TOKEN}" "https://your.gitlab.instance.com/api/v4/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/approval_state > approvals_output
- REQUIRED_APPROVALS = $(cat approvals_output | jq '.["rules"][]["approvals_required"]')
- APPROVALS_COUNT = $(cat approvals_output | jq '.["rules"][]["approved_by"] | length')
- RUN_PIPELINE=false; if ["$APPROVALS_COUNT" -ge "$REQUIRED_APPROVALS"] then
RUN_PIPELINE=true
fi
- echo "RUN_PIPELINE=${RUN_PIPELINE}" >> variables
artifacts:
reports:
dotenv: variables
The dotenv
report type lets us define variables in one job stage, and share them in all later stages. By putting this job in the first stage, all other stages in the pipeline will have access to the $RUN_PIPELINE
variable. For the example above, after getting the approval values from the API we compare the number of approvals to the number required. If approvals is greater than or equal to the required number of approvals, we set our variable to true. Otherwise it's false.
By default, we set this job to never run, but then check to see if the $CI_PIPELINE_SOURCE
variable, which contains the event that kicked off the pipeline, is a merge_request_event
. If it is, we run the job. This ensures that the $CI_MERGE_REQUEST_IID
variable will be set.
Now, in all other jobs that should only run if the Merge Request is approved, we can check the value of the variable:
Deploy to Prod:
stage: deploy
when: never
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event' && $RUN_PIPELINE"
when: always
script:
- ...
First we check the pipeline source again since the $RUN_PIPELINE
variable won't exist if it isn't, then we check if $RUN_PIPELINE
is true. If it is, we run the job, otherwise the default is to never run.
Resources:
Merge Request Approvals API
Predefined Variables
The rules
keyword
The when
keyword
The dotenv
Report Type
The jq
manual