DEV Community

Marcin Wosinek
Marcin Wosinek

Posted on • Originally published at how-to.dev

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
Enter fullscreen mode Exit fullscreen mode

Then, let's create an executable file:

$ touch rebase.js
$ chmod +x rebase.js
Enter fullscreen mode Exit fullscreen mode

The file content is as follow:

#!/usr/bin/env node
Enter fullscreen mode Exit fullscreen mode

Set's the command to be run when we execute the file.

const fetch = require("node-fetch");
Enter fullscreen mode Exit fullscreen mode


js
node-fetch, node implementation of fetch API from the browser.

const projectId = process.argv[2] ? process.argv[2] : process.env.CI_PROJECT_ID,
  apiToken = process.argv[3] ? process.argv[3] : process.env.API_TOKEN,
Enter fullscreen mode Exit fullscreen mode

The script supports 2 ways of providing necessary values:

$ export CI_PROJECT_ID="28869171"
$ export API_TOKEN="secret-key"
$ ./rebase.js
Enter fullscreen mode Exit fullscreen mode

or:

$ ./rebase.js 28869171 secret-key
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 },
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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;
          })
Enter fullscreen mode Exit fullscreen mode

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);
      })
    );
  })
Enter fullscreen mode Exit fullscreen mode

Log error & catch failure.

  .then((resultSummary) => {
    console.log(resultSummary);
  })
  .catch((error) => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

GitLab CI configuration

image: node:16
Enter fullscreen mode Exit fullscreen mode
stages:
  - build
  - test
Enter fullscreen mode Exit fullscreen mode
build:
  stage: build
Enter fullscreen mode Exit fullscreen mode
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: always
Enter fullscreen mode Exit fullscreen mode
  script:
    - npm ci
    - ./rebase.js
Enter fullscreen mode Exit fullscreen mode
test:
  stage: test
  script:
    - echo 'test run'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 } ]
Enter fullscreen mode Exit fullscreen mode

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

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

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.

Discussion (0)