How to autorebase MRs in GitLab CI
Tags: gitlab, ci
Published: 2021-08-15 13:32:37

In this article, I'll show you how to autorebase MRs in GitLab CI.

# The problem
If you:
* try to keep your git history linear
* use merge requests (MR) for code reviews & CI
* your CI runs in nontrivial time 

Whenever you have more than 1 MR for every merged MR, you will have to rebase all the others. In my team, we manage to keep CI time below 30 minutes, but with just 3~4 MRs queued, it's becoming a headache to merge/rebase every now & then.

# The idea
The best solution would be to automate the rebases. We could have an external server integrated with GitLab and rebase our code when something new is merged to the main branch. Or, we start light and use our own CI to run rebases. So we can:
* have a CI job that runs at the beginning of the main branch pipeline
* get's all open MR with the [rest API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests)
* calls [the rebase endpoint](https://docs.gitlab.com/ee/api/merge_requests.html#rebase-a-merge-request) for all MRs

# Code
I'll implement a solution in JavaScript. First, let's generate an npm package & install dependency:
```sh
$ npm run -y
$ npm install --save-dev node-fetch
```

Then, let's create an executable file:
```sh
$ touch rebase.js
$ chmod +x rebase.js
```

The file content is as follow:
```js
#!/usr/bin/env node
```
Set's the command to be run when we execute the file.

```
const fetch = require("node-fetch");
```js
[node-fetch](https://www.npmjs.com/package/node-fetch), node implementation of fetch API from the browser.

```js
const projectId = process.argv[2] ? process.argv[2] : process.env.CI_PROJECT_ID,
  apiToken = process.argv[3] ? process.argv[3] : process.env.API_TOKEN,
```

The script supports 2 ways of providing necessary values:
```sh
$ export CI_PROJECT_ID="28869171"
$ export API_TOKEN="secret-key"
$ ./rebase.js
```

or:
```sh
$ ./rebase.js 28869171 secret-key
```

The first way mimics how it will run on GitLab CI agent; the second way is easier to test locally.

```js
  apiV4Url = process.env.CI_API_V4_URL
    ? process.env.CI_API_V4_URL
    : "https://gitlab.com/api/v4";
```

A nod for self-hosted GitLab instances. Everybody else will be fine with the default value.

```js
function callApi(command, method = "GET") {
  console.log("query", apiV4Url + "/projects/" + projectId + command);

  return fetch(apiV4Url + "/projects/" + projectId + command, {
    method,
    headers: { "PRIVATE-TOKEN": apiToken },
  });
}
```
Helper method to avoid code duplication in our short script.

```js
callApi("/merge_requests?state=opened")
  .then((response) => response.json())
  .then((response) => {
    const iids = response.map((mr) => mr.iid);
```
Querry all open MRs & turned returned values into an array of iid - local ids. 

```js
    return Promise.all(
      iids.map((iid) => {
        return callApi(`/merge_requests/${iid}/rebase`, "PUT")
```
For each `iid`, we call rebase. It fails gracefully, so the queries don't crash if the MR cannot be rebased.

```js
          .then((response) => response.json())
          .then((response) => {
            response.iid = iid;

            return response;
          })
```
The response is terse (`[{ rebase_in_progress: true }]`), I'm adding the iid, so at least we can tell what MRs are being rebased.

```js
          .catch(console.error);
      })
    );
  })
```
Log error & catch failure.

```js
  .then((resultSummary) => {
    console.log(resultSummary);
  })
  .catch((error) => {
    console.error(error);
  });
```
Display result to the screen.

## Complete `rebase.js`
```js
#!/usr/bin/env node

const fetch = require("node-fetch");

const projectId = process.argv[2] ? process.argv[2] : process.env.CI_PROJECT_ID,
  apiToken = process.argv[3] ? process.argv[3] : process.env.API_TOKEN,
  apiV4Url = process.env.CI_API_V4_URL
    ? process.env.CI_API_V4_URL
    : "https://gitlab.com/api/v4";

function callApi(command, method = "GET") {
  console.log("query", apiV4Url + "/projects/" + projectId + command);

  return fetch(apiV4Url + "/projects/" + projectId + command, {
    method,
    headers: { "PRIVATE-TOKEN": apiToken },
  });
}

callApi("/merge_requests?state=opened")
  .then((response) => response.json())
  .then((response) => {
    const iids = response.map((mr) => mr.iid);

    return Promise.all(
      iids.map((iid) => {
        return callApi(`/merge_requests/${iid}/rebase`, "PUT")
          .then((response) => response.json())
          .then((response) => {
            response.iid = iid;

            return response;
          })
          .catch(console.error);
      })
    );
  })
  .then((resultSummary) => {
    console.log(resultSummary);
  })
  .catch((error) => {
    console.error(error);
  });
```

# GitLab CI configuration
```yml
image: node:16
```

```yml
stages:
  - build
  - test
```

```yml
build:
  stage: build
```

```yml
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: always
```

```yml
  script:
    - npm ci
    - ./rebase.js
```

```yml
test:
  stage: test
  script:
    - echo 'test run'
```

## Complete `.gitlab-ci.yml`
```yml
image: node:16

stages:
  - build
  - test

build:
  stage: build
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: always
  script:
    - npm ci
    - ./rebase.js

test:
  stage: test
  script:
    - echo 'test run'
```

# Script output

Example output of successfully run script:
```sh
query https://gitlab.com/api/v4/projects/28869171/merge_requests?state=opened
query https://gitlab.com/api/v4/projects/28869171/merge_requests/2/rebase
[ { rebase_in_progress: true, iid: 2 } ]
```

# Configuration
For this script to run, we have to create an API token. Here you can create [personal access token](https://gitlab.com/-/profile/personal_access_tokens):
![personal-token.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uim6cz05f7vvgkrm56bj.png)

After creating the token, you have to add it as `API_TOKEN` to CI variables. It would help to make the variable 'masked' so it will not appear in the CI logs by accident.
![adding-variable.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zb4qhula9cifgjegqh9x.png)

In the case of my repo, the ULR is https://gitlab.com/how-to.dev/autorebase-merge-requests/-/settings/ci_cd.

For running the script outside of CI, you have to set project id. It's visible on the main page of project settings:
![project-id.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccqlfxaxe8dqjdtjgf6g.png)

For my repo, the URL is https://gitlab.com/how-to.dev/autorebase-merge-requests/edit.

# Links
* [GitLab merge request api docs](https://docs.gitlab.com/ee/api/merge_requests.html)
* [GitLab CI variables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)
* [Example repo](https://gitlab.com/how-to.dev/autorebase-merge-requests)

# Summary
In this article, we have seen how to add a simple autorebase to our GitLab CI. If you would be interested in a GitLab video course, let me know by registering [here](https://landing.mailerlite.com/webforms/landing/n2r8t5).