How to autorebase MRs in GitLab CI

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
  • calls the rebase endpoint for all MRs

Code

I'll implement a solution in JavaScript. First, let's generate an npm package & install dependency:

$ npm run -y
$ npm install --save-dev node-fetch

Then, let's create an executable file:

$ touch rebase.js
$ chmod +x rebase.js

The file content is as follow:

#!/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:

$ export CI_PROJECT_ID="28869171"
$ export API_TOKEN="secret-key"
$ ./rebase.js

or:

$ ./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.

  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.

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.

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.

    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.

          .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.

          .catch(console.error);
      })
    );
  })

Log error & catch failure.

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

Display result to the screen.

Complete rebase.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

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'

Complete .gitlab-ci.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:

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: personal-token.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

In the case of my repo, the ULR is gitlab.com/how-to.dev/autorebase-merge-requ...

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

For my repo, the URL is gitlab.com/how-to.dev/autorebase-merge-requ...

Links

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: