How to Build a ChatGPT-Powered Code Review Bot for GitLab with Node.js
This article walks through creating a GitLab code‑review bot that leverages the ChatGPT API, detailing request handling with Axios, constructing prompts, interacting with both ChatGPT and GitLab APIs, and posting automated review comments back to merge requests.
This personal learning summary documents the implementation of a ChatGPT‑based code review feature in GitLab, primarily to explore ChatGPT.
Data Request
Instead of the chatgpt library (which requires Node ≥ 18 and may encounter fetch issues behind firewalls), a simple Axios wrapper is used to handle HTTP requests.
import axios from 'axios';
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
const createRequest = (host, { headers, data, params }) => {
const instance = axios.create({ baseURL: host });
instance.interceptors.request.use(
config => {
if (params) config.params = { ...params, ...config.params };
if (headers) config.headers.set(headers);
if (data) config.data = { ...data, ...config.data };
return config;
},
error => Promise.reject(error)
);
instance.interceptors.response.use(
response => response,
error => {
console.log(error);
return Promise.reject(error);
}
);
return instance;
};
export default createRequest;This wrapper is later used for both the ChatGPT and GitLab API calls.
ChatGPT API
The official ChatGPT API can be tested with a simple curl command; a valid API key is required. The /v1/chat/completions endpoint is used to send code patches for review.
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello!"}]
}'A ChatGPT class is defined to encapsulate request creation, prompt generation, and the codeReview method:
import createRequest from './request';
import { logger } from './utils';
import type { AxiosInstance } from 'axios';
import type { ChatGPTConfig } from './types';
export default class ChatGPT {
private language: string;
private request: AxiosInstance;
constructor(config: ChatGPTConfig) {
const host = 'https://api.openai.com';
this.request = createRequest(host, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
data: {
model: config.model || 'gpt-3.5-turbo',
temperature: +(config.temperature || 0) || 1,
top_p: +(config.top_p || 0) || 1,
presence_penalty: 1,
stream: false,
max_tokens: 1000,
},
});
this.language = config.language || 'Chinese';
}
private generatePrompt = (patch: string) => {
const answerLanguage = `Answer me in ${this.language},`;
return `Below is the gitlab code patch, please help me do a brief code review, ${answerLanguage} if any bug risk and improvement suggestion are welcome
${patch}`;
};
private async sendMessage(msg: string) {
const currentDate = new Date().toISOString().split('T')[0];
return this.request.post('/v1/chat/completions', {
messages: [
{
role: 'system',
content: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.
Knowledge cutoff: 2021-09-01
Current date: ${currentDate}`,
},
{ role: 'user', content: msg, name: undefined },
],
});
}
public async codeReview(patch: string) {
if (!patch) {
logger.error('patch is empty');
return '';
}
const prompt = this.generatePrompt(patch);
const res = await this.sendMessage(prompt);
const { choices } = res.data;
if (Array.isArray(choices) && choices.length > 0) {
return choices[0]?.message?.content;
}
return '';
}
}The codeReview method sends the code diff to ChatGPT and returns the review text.
GitLab API
Two endpoints are used: one to fetch merge‑request changes and another to post discussion comments.
Fetch changes: /api/v4/projects/${projectId}/merge_requests/${mergeRequestIId}/changes
Post comment: /api/v4/projects/${projectId}/merge_requests/${mergeRequestIId}/discussions
The helper parseLastDiff extracts the last line numbers from a diff string.
const parseLastDiff = (gitDiff) => {
const diffList = gitDiff.split('
').reverse();
const lastLineFirstChar = diffList?.[1]?.[0];
const lastDiff = diffList.find(item => /^@@ -\d+,\d+ \+\d+,\d+ @@/g.test(item)) || '';
const [lastOldLineCount, lastNewLineCount] = lastDiff
.replace(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*/g, (_, a, b, c, d) => `${+a + +b},${+c + +d}`)
.split(',');
if (!/^\d+$/.test(lastOldLineCount) || !/^\d+$/.test(lastNewLineCount)) {
return { lastOldLine: -1, lastNewLine: -1 };
}
const lastOldLine = lastLineFirstChar === '+' ? -1 : (parseInt(lastOldLineCount) || 0) - 1;
const lastNewLine = lastLineFirstChar === '-' ? -1 : (parseInt(lastNewLineCount) || 0) - 1;
return { lastOldLine, lastNewLine };
};A Gitlab class wraps the API calls:
import camelCase from 'camelcase';
import createRequest from './request';
import { logger } from './utils';
import type { GitlabConfig, GitlabDiffRef, GitlabChange } from './types';
import type { AxiosInstance } from 'axios';
const formatByCamelCase = (obj) => {
const target = Object.keys(obj).reduce((result, key) => {
const newkey = camelCase(key);
return { ...result, [newkey]: obj[key] };
}, {});
return target;
};
export default class Gitlab {
private projectId;
private mrIId;
private request: AxiosInstance;
private target: RegExp;
constructor({ host, token, projectId, mrIId, target }: GitlabConfig) {
this.request = createRequest(host, { params: { private_token: token } });
this.mrIId = mrIId;
this.projectId = projectId;
this.target = target || /\.(j|t)sx?$/;
}
getChanges() {
return this.request
.get(`/api/v4/projects/${this.projectId}/merge_requests/${this.mrIId}/changes`)
.then(res => {
const { changes, diff_refs: diffRef, state } = res.data;
const codeChanges = changes
.map(item => formatByCamelCase(item))
.filter(item => {
const { newPath, renamedFile, deletedFile } = item;
if (renamedFile || deletedFile) return false;
return this.target.test(newPath);
})
.map(item => {
const { lastOldLine, lastNewLine } = parseLastDiff(item.diff);
return { ...item, lastNewLine, lastOldLine };
});
return { state, changes: codeChanges, ref: formatByCamelCase(diffRef) as GitlabDiffRef };
})
.catch(error => {
logger.error(error);
return { state: '', changes: [], ref: {} as GitlabDiffRef };
});
}
postComment({ newPath, newLine, oldPath, oldLine, body, ref }) {
return this.request
.post(`/api/v4/projects/${this.projectId}/merge_requests/${this.mrIId}/discussions`, {
body,
position: {
position_type: 'text',
base_sha: ref?.baseSha,
head_sha: ref?.headSha,
start_sha: ref?.startSha,
new_path: newPath,
new_line: newLine,
old_path: oldPath,
old_line: oldLine,
},
})
.catch(error => logger.error(error));
}
async codeReview({ change, message, ref }) {
const { lastNewLine = -1, lastOldLine = -1, newPath, oldPath } = change;
if (lastNewLine === -1 && lastOldLine === -1) {
logger.error('Code line error');
return;
}
const params = {} as any;
if (lastOldLine !== -1) {
params.oldLine = lastOldLine;
params.oldPath = oldPath;
}
if (lastNewLine !== -1) {
params.newLine = lastNewLine;
params.newPath = newPath;
}
return await this.postComment({ ...params, body: message, ref });
}
}Combined Workflow
Retrieve merge‑request changes via the GitLab client.
Send each diff to the ChatGPT client for review.
Write the review comments back to the corresponding diff lines in the merge request.
async function run({ gitlabConfig, chatgptConfig }) {
const gitlab = new Gitlab(gitlabConfig);
const chatgpt = new ChatGPT(chatgptConfig);
const { state, changes, ref } = await gitlab.getChanges();
if (state !== 'opened') {
logger.log('MR is closed');
return;
}
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
const message = await chatgpt.codeReview(change.diff);
const result = await gitlab.codeReview({ message, ref, change });
logger.info(message, result?.data);
}
}Review Results
ChatGPT provides explanations, highlights potential risks, and points out style issues (e.g., misspelled class names). For large merge requests, an automated AI review can significantly reduce manual effort.
The code resides in a public GitLab repository, so it can be accessed directly; private GitLab instances may require VPN and must consider security compliance. The workflow can also be triggered via GitLab CI.
For more details, see the source repository at https://github.com/ikoofe/chat-review .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
