3

I have created a project in gitlab and have the following entry in the gitlab.yaml file. After I push any changes in the project, the pipeline kicks in. Is it possible to change this so the pipeline stats only after the user submits a merge request and it is approved?

Here is my .gitlab-ci.yaml file:

stages:
  - Test
  - Staging
  - Prod

Test:
    stage: Test
    tags:
    - test
    script:
    - cd app_directory
    - cd Test
    - git checkout
    - git pull
    except:
    - master
    when: always
    only:
    - Test  
    only:
    - merge_requests
    allow_failure: false    
Staging:
    stage: Staging
    tags:
    - staging
    script:
    script:
    - cd app_directory
    - cd Staging
    - git checkout
    - git pull
    except:
    - master
    when: always
    only:
    - Test  
    only:
    - merge_requests
    allow_failure: false    
Prod:
    stage: Prod
    tags:
    - Prod
    script:
    - cd app_directory
    - cd Prod
    - git checkout
    - git pull
    except:
    - master
    when: always
    only:
    - Test  
    only:
    - merge_requests
    allow_failure: false    
variables:
    GIT_STRATEGY: clone
Adam Marshall
  • 6,369
  • 1
  • 29
  • 45
user1695083
  • 41
  • 1
  • 3

2 Answers2

3

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

Flair
  • 2,609
  • 1
  • 29
  • 41
Adam Marshall
  • 6,369
  • 1
  • 29
  • 45
1

It is possible to get a pipeline that either totally relies on merge approvals, or partially does, depending upon your needs. Personally, I have a set of code quality checks I want to run unconditionally for every commit, and some more comprehensive tests + candidate build job I want to run after the merge approvals are satisfied.

The key to these pipelines is using the variable CI_MERGE_REQUEST_APPROVED in your workflow rules, or your job rules. Unfortunately, this variable does not exist at all until the merge request approvals are completed. Then its value will be true. That means that if you use it in workflow rules that can prevent the pipeline from running, as of today GitLab's "pipelines" tab in the merge request won't be available to manually run it after the merge approvals are completed. If your pipeline runs and has a failed job in it, the pipelines tab will show up, and you can re-execute that job when the approvals are in to complete the pipeline run.

To enable this capability, you need to avoid using CI_MERGE_REQUEST_APPROVED in your workflow/job rules, and check this variable in the script portion of the job you want to fail if the approvals are not yet completed. This singular line in scripts will need to see if the variable==true, and if it does not this line needs to return a value of 1.

I do this with a file in my repository that creates a function in bash to check this value, which returns the appropriate value.

#!/bin/bash

function check_approvals {
    if [ "$CI_MERGE_REQUEST_APPROVED" = 'true' ]; then printf "Merge Request is approved.\n" && return 0; else printf "Merge Request not approved yet.\n" && return 1; fi
}

check_approvals

My script line that includes this functionality is: source ./pipelines/mr-check-approvals.sh

GitLab has an open issue that will help better enable this functionality, and allow us to use CI_MERGE_REQUEST_APPROVED in workflow/pipeline rules. https://gitlab.com/gitlab-org/gitlab/-/issues/329787

Flair
  • 2,609
  • 1
  • 29
  • 41