Work with a Merge Request in GitLab CI/CD Pipeline

I started out writing a naive bot in GitLab CI/CD pipeline that would provide informational feedback about a merge request (MR) in the MR. Since this was my first attempt writing such a thing, I kept the scope limited and simple yet effective.

The basic scope of the bot was,

  • Run the bot in the context of an MR (see Rules below)
  • Work only with one MR, the one running the pipeline
  • Use GitLab REST APIs to read from the MR and write to it
  • Use bash, curl, git, and jq. I could have used Python or Go but this was a naive bot so I couldn't make it simpler than this.

In this post I will not provide the code. Instead, I will provide enough information for anyone to get started writing their own bot.

Predefined Variables

GitLab pipelines provide a lot of predefined variables. I highly enocurage you to review the list and make use of them. I found through experience that a lot of the features I expected to be available in the REST APIs, or would have to write git commands for, were available as data in the form of predefined variables.

The variables I used were,

  • CI_API_V4_URL
  • CI_MERGE_REQUEST_DIFF_BASE_SHA
  • CI_MERGE_REQUEST_IID
  • CI_PROJECT_ID

For example, the base URL I created to work with MRs looked like,

"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}"

API Endpoints

Since this post is about working with MRs, I only used the Merge requests API endpoints. They were,

diffs

I wanted to use the /diffs endpoint as it gives structured data about the diff in the MR. One thing I was looking for was the names of files that were changed in the MR and this endpoint gave me exactly what I needed.

$ curl -X GET "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/diffs" | jq '.[] | select(.new_path=="SOME-FILE-NAME") | .diff' | wc -w

One version of the bot I wrote used git to give me the list of files that had changed. The easiest way to do that in the pipeline was to use the predefined variable CI_MERGE_REQUEST_DIFF_BASE_SHA,

$ git diff --name-only "${CI_MERGE_REQUEST_DIFF_BASE_SHA}"

discussions and notes

These are related but different. To start a new thread in the MR, use /discussions. To delete it, use /notes. To list all comments (including threads), use /notes.

$ curl -X POST --header 'Content-Type: application/json' --data '{"body": "YOUR TEXT FOR THREAD/COMMENT"}' "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/discussions"

$ curl -X GET "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes" | jq '.[] | select(.type=="DiscussionNote" and .body=="YOUR TEXT FOR THREAD/COMMENT" and .resolved==false) | .id'

$ curl -X DELETE "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes/:id"

labels

I also added and removed labels. Use Update MR for that use case.

$ curl -X PUT --header 'Content-Type: application/x-www-form-urlencode' --data 'add_labels=YOUR-LABEL' "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}"

Authentication

This was the most interesting part. When a pipeline is running it gets a predefined variable called CI_JOB_TOKEN. This token can be used to authenticate to GitLab REST APIs. However, it has a drawback because GitLab doesn't allow access to all REST API endpoints with this authentication method. Sadly, it doesn't support merge request API endpoints.

There are three other options available,

As the names suggest, these access tokens have differing scopes but provide the same ability: authenticate to GitLab and use REST APIs. They all support merge request API endpoints.

Since I was writing a shared pipeline, I didn't want to use my personal access token. I didn't have enough permissions in the GitLab group to create group access token. That left me with project access token because I was a maintainer on the project and had access to create it.

I created the token and injected it into the pipeline using a CI/CD variable. The variable was called GITLAB_TOKEN, following the convention used by glab (Authenticate with GitLab).

To authenticate I used a header with curl everytime,

$ curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" # ... rest of the command

Rules

I wanted to run the stage only in MRs. The rules section in .gitlab-ci.yml had the following rule,

rules:
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'