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.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
How to Build a ChatGPT-Powered Code Review Bot for GitLab with Node.js
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 .

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.jscode reviewGitLabChatGPTaxiosAPI Integration
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.