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, 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,
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:
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.
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:
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.
Top comments (0)