GitLab Runnerを使って、ChatGPTにマージリクエストのレビューをしてもらう / 作業ログ編

はじめに

完成記事
※この記事は作業ログになりますので、試行錯誤の記録になります。

ChatGPTを使用して、マージリクエストのレビューをしてもらいたいという気持ちがあり、
実際にできそうなので構築をしてみる。

環境

Windows 10 Professional
WSL2 (Ubuntu22.04 LTS)
docker version 23.0.5
Docker Compose version v2.17.3

GitLab
gitlab-runner latest

GitLab-runner の登録

  1. gitlab-runner コンテナへ入る。
docker compose exec runner bash
  1. gitlab-runner register を実行する
root@1ce083816d04:/# gitlab-runner register
  1. URL と token を入力する。
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://gitlab.example.com/
Enter the registration token:
XXXXXXXXXXXX
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://gitlab.example.com/
Enter the registration token:
XXXXXXXXXXXX
Verifying runner... is valid                        runner=oLvY-r6z9
Enter a name for the runner. This is stored only in the local config.toml file:
[0fa8a2573a3b]: mr-review
Enter an executor: ssh, docker-autoscaler, docker+machine, instance, custom, docker, parallels, kubernetes, docker-windows, shell, virtualbox:
docker
Enter the default Docker image (for example, ruby:2.7):
python:3.11.4-buster
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"
root@0fa8a2573a3b:/#
項目名設定値
URLhttp://gitlab.example.com/
tokenXXXXXXXXXXXX
runner namemr-review
executordocker
Docker imagepython:3.11.4-buster

imagepython3.11.4-buster を使用した。DockerHub から使用できるイメージを参照する。

Personal Access Tokenを取得する

personal_access_token personal_access_token_2

CI/CDの設定

プロジェクト → 「CI/CD」から「Variables」で変数を設定する。
openai_api_key_variable

personal_access_token_variables

エラー

Docker が実行できないみたいなエラー

  runner:
    image: 'gitlab/gitlab-runner:latest'
    restart: always
    volumes:
      - ./srv/gitlab-runner/config:/etc/gitlab-runner
+      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - "8093:8093"

ホスト側の docker.sockをマウントする必要がある。
そうすることで、ホスト側の Docker のソケットを共有し、gitlab-runnerコンテナからもdockerが使用できる。

gitlab.example.com で gitlab のコンテナアクセスする。

  runner:
    image: 'gitlab/gitlab-runner:latest'
    restart: always
    volumes:
      - ./srv/gitlab-runner/config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      - "8093:8093"
+    links:
+      - "gitlab:gitlab.example.com"

アクセスできるように

docker compose exec runner bash
root@d824f2d5f224:/# curl http://gitlab.example.com/
<html><body>You are being <a href="http://gitlab.example.com/users/sign_in">redirected</a>.</body></html>root@d824f2d5f224:/#

※しかし、後述の手順のエラーを解消できるわけではないので、必ずしも設定する必要はなさそう。

JobでGit cloneができないエラー

CI/CD が走ったあとに、Job では下記のエラーが出た。

Running with gitlab-runner 16.0.2 (85586bd1)
  on 7bc6a94280ca jLhReocfx, system ID: r_clhhKBEcv5mo
Preparing the "docker" executor
00:04
Using Docker executor with image python:3.11.4-buster ...
Pulling docker image python:3.11.4-buster ...
Using docker image sha256:5bf9a636079f0257fa52c3bbed42ec29d3b975cfd1ef2db2016c1bf598492d97 for python:3.11.4-buster with digest python@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666 ...
Preparing environment
00:01
Running on runner-jlhreocfx-project-3-concurrent-0 via d824f2d5f224...
Getting source from Git repository
00:01
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /builds/root/mr-review-test/.git/
fatal: unable to access 'http://gitlab.example.com/root/mr-review-test.git/': Failed to connect to gitlab.example.com port 80 after 112 ms: Couldn't connect to server
ERROR: Job failed: exit code 1

gitlab.example.com の ドメインに対する git clone ができていないみたい。

runner コンテナで git clone ができるのかを調査。

docker compose exec runner bash

git clone http://gitlab.example.com/root/mr-review-test.git/

root@d824f2d5f224:/# git clone http://gitlab.example.com/root/mr-review-test.git/
Cloning into 'mr-review-test'...
Username for 'http://gitlab.example.com':

いけるみたい。
gitlab-runner ユーザでプロジェクトが見えない可能性?
gitlab-runner で使用しているGitのユーザは何なのか?

※プロジェクトの可視性を public にしてみたけどだめだった。

解決方法
url, clone_url をホストの IP にした。
ドメインだとうまく解決できないみたい。
runner のコンテナに入ると gitlab.example.com はうまく行っているのだが・・・。

toml を下記のように修正, url, clone_url を修正した。

[[runners]]
  name = "7bc6a94280ca"
  url = "http://192.168.0.20"
  id = 3
  token = "glrt-jLhReocfxa-C7VJbP1ws"
  token_obtained_at = 2023-06-14T10:36:53Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  clone_url = "http://192.168.0.20"
  [runners.cache]
    MaxUploadedArchiveSize = 0
  [runners.docker]
    tls_verify = false
    image = "python:3.11.4-buster"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0

.gitlab-ci.yml

stages:
  - test

test:
  stage: test
  script:
    - python mr_review.py

build:
  script:
    - echo Hello,World
#    - python mr_review.py

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script: echo "Dummy job for merge-request"

うまく行っているみたい。 testのジョブ

Running with gitlab-runner 16.0.2 (85586bd1)
  on 7bc6a94280ca jLhReocfx, system ID: r_clhhKBEcv5mo
Preparing the "docker" executor
00:04
Using Docker executor with image python:3.11.4-buster ...
Pulling docker image python:3.11.4-buster ...
Using docker image sha256:5bf9a636079f0257fa52c3bbed42ec29d3b975cfd1ef2db2016c1bf598492d97 for python:3.11.4-buster with digest python@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666 ...
Preparing environment
00:01
Running on runner-jlhreocfx-project-3-concurrent-0 via d824f2d5f224...
Getting source from Git repository
00:01
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /builds/root/mr-review-test/.git/
Checking out da64e4af as detached HEAD (ref is main)...
Skipping Git submodules setup
Executing "step_script" stage of the job script
00:02
Using docker image sha256:5bf9a636079f0257fa52c3bbed42ec29d3b975cfd1ef2db2016c1bf598492d97 for python:3.11.4-buster with digest python@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666 ...
$ python mr_review.py
Hello,World
Job succeeded

buildのジョブ

Running with gitlab-runner 16.0.2 (85586bd1)
  on 7bc6a94280ca jLhReocfx, system ID: r_clhhKBEcv5mo
Preparing the "docker" executor
00:04
Using Docker executor with image python:3.11.4-buster ...
Pulling docker image python:3.11.4-buster ...
Using docker image sha256:5bf9a636079f0257fa52c3bbed42ec29d3b975cfd1ef2db2016c1bf598492d97 for python:3.11.4-buster with digest python@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666 ...
Preparing environment
00:01
Running on runner-jlhreocfx-project-3-concurrent-0 via d824f2d5f224...
Getting source from Git repository
00:02
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /builds/root/mr-review-test/.git/
Checking out da64e4af as detached HEAD (ref is main)...
Skipping Git submodules setup
Executing "step_script" stage of the job script
00:01
Using docker image sha256:5bf9a636079f0257fa52c3bbed42ec29d3b975cfd1ef2db2016c1bf598492d97 for python:3.11.4-buster with digest python@sha256:3a19b4d6ce4402d11bb19aa11416e4a262a60a57707a5cda5787a81285df2666 ...
$ echo Hello,World
Hello,World
Job succeeded

GitLabAPI を使うテスト

$ python mr_review.py
Traceback (most recent call last):
  File "/builds/root/mr-review-test/mr_review.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

pip install する必要がある。

test:
  stage: test
  script:
+   - pip install -r requirements.txt
    - python mr_review.py

build:
  script:
    - echo Hello,World

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script: echo "Dummy job for merge-request"

上記を追加する。

requirements.txt は下記のとおりに作成した。

requests
openai

ymlを書き直し。

build:
  script:
    - echo Hello,World

mr-review-gpt:
  stage: test
  script:
    - pip install -r requirements.txt
    - python mr_review.py
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

これで、 mr-review-gptはマージリクエストを作成したときのみ実行される。

マージリクエストを作成する

適当なブランチを作ってプッシュし、マージリクエストを作成する。

merge-requests
merge-requests-2
merge-requests-3

$ python mr_review.py
Traceback (most recent call last):
  File "/builds/root/mr-review-test/mr_review.py", line 16, in <module>
    api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'
                                                                          ^^^^^^^^^^^^^^^^^
NameError: name 'merge_request_iid' is not defined. Did you mean: 'merge_request_id'?
merge-requests-job_failed

エラーが出るのでスクリプトを見直す。

merge_request_id なのに、 merge_request_iid で参照していたので修正した。

- api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'
+ api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_id}/changes'

gitlab.example.comが参照できないエラー

requests.exceptions.ConnectionError: HTTPConnectionPool(host='gitlab.example.com', port=80): Max retries exceeded with url: /api/v4/projects/3/merge_requests/4/changes (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f228ca49510>: Failed to establish a new connection: [Errno 111] Connection refused'))

gitlab.example.com が参照できないエラー。
うーん、やっぱりホストの IP アドレスで直接参照させるのが良さそうか。
というわけでスクリプトのurlを変える。

Successfully installed aiohttp-3.8.4 aiosignal-1.3.1 async-timeout-4.0.2 attrs-23.1.0 certifi-2023.5.7 charset-normalizer-3.1.0 frozenlist-1.3.3 idna-3.4 multidict-6.0.4 openai-0.27.8 requests-2.31.0 tqdm-4.65.0 urllib3-2.0.3 yarl-1.9.2
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
$ python mr_review.py
{'message': '404 Project Not Found'}
Job succeeded

できた、が Project Not Found になった・・・!
あと、piprootで実行しないで、venv使ってみたいなのが出た。

venv を使うように修正

とりあえず、 venv使うように ymlを修正する。

+before_script:
+  - python -V
+  - pip install virtualenv
+  - virtualenv venv
+  - source venv/bin/activate
before_script:
  - python -V
  - pip install virtualenv
  - virtualenv venv
  - source venv/bin/activate

build:
  script:
    - echo Hello,World

mr-review-gpt:
  stage: test
  script:
    - pip install -r requirements.txt
    - python mr_review.py
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

これで良さそう。

Project Not Found を直す。

おそらく、 Personal Access Tokenが悪そうか?

下記でアクセストークンとかを表示させてみた。

print(access_token)
print(project_id)
print(merge_request_id)
$ python mr_review.py
None
3
4
{'message': '404 Project Not Found'}

コンソールを見ると、 Noneになっていた。
これが原因みたい。

どうやら、「CI/CD」の設定の「Variables」で下記のチェックが入っていたので変数が展開されなかったみたい。
ci_cd_project_variable

変更すると下記のように表示された。

$ python mr_review.py
[MASKED]glpat-
3
4
{'error': 'insufficient_scope', 'error_description': 'The request requires higher privileges than provided by the access token.', 'scope': 'api read_api'}

今度は、アクセストークンの権限が足りないみたい。

read_repository, write_repository, admin_mode

しか許可していないので・・・。
最終的に、 MR に書き込みをするので、 「api」の権限を与える。

api, read_api, read_repository, write_repository, admin_mode

が最終的な権限。

変わらずだめみたい。

$ python mr_review.py
[MASKED]glpat-
3
4
{'message': '404 Not found'}

https://gitlab.com/gitlab-org/gitlab-foss/-/issues/29022
↑ みたいに、同じ人がいるみたいだ。
解決方法は・・・?

curl でアクセスできるか試す。

curl --header "PRIVATE-TOKEN: <TOKEN>" <API_ENDPOINT>
$ curl --header "PRIVATE-TOKEN: glpat-M32nMy-Axbe6q-hzqCd8" http://192.168.0.20/api/v4/projects/3/merge_requests/4/changes
{"message":"404 Not found"}

curlでもアクセスできないみたい。

この API URL になにか問題があるのかを調査。
ChatGPT にきいたやつなので、情報が古いかもと思い、API の Reference を見る。
https://docs.gitlab.com/ee/api/rest/
https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr
GET /projects/:id/merge_requests/:merge_request_iid みたい。

curl "http://192.168.0.20/api/v4/projects?private_token=<your_access_token>"
curl --header "PRIVATE-TOKEN: <your_access_token>" "http://192.168.0.20/api/v4/projects"

どっちもアクセスできた。JSON が返ってきた。

PersonalAccessTokenなどの問題ではなさそう。

curl --header "PRIVATE-TOKEN: <token>" "http://192.168.0.20/api/v4/projects/3/merge_requests/3/changes"

projects の ID と MR の ID を適当に変えて実行してみた。
※Web 上で見える ID と JOB で取得される、 CI_MERGE_REQUEST_IDが異なっているから。

適当に変えたら取れた。
ということは、JOB で取得される MR の ID が悪そうだ。

API の URL を見る限り、 CI_MERGE_REQUEST_ID ではなく、 CI_MERGE_REQUEST_IIDを使う必要がありそうだ。

ChatGPT に相違点を聞いてみる。

CI_MERGE_REQUEST_IDとCI_MERGE_REQUEST_IIDは、GitLab CI/CDの環境変数であり、マージリクエストに関連する情報を提供します。以下にそれぞれの変数の意味と異なる点を説明します。

CI_MERGE_REQUEST_ID: この環境変数は、現在実行中のジョブが関連付けられているマージリクエストのID(Identifiers)を表します。マージリクエストIDは、プロジェクト内で一意の数値であり、マージリクエストを一意に特定するために使用されます。

CI_MERGE_REQUEST_IID: この環境変数は、現在実行中のジョブが関連付けられているマージリクエストのIID(Internal ID)を表します。マージリクエストIIDは、プロジェクト内でのマージリクエストの相対的な識別子であり、そのプロジェクト内で一意です。マージリクエストIIDは、特定のプロジェクトのマージリクエストの連続的なナンバリングを表します。

異なる点:

CI_MERGE_REQUEST_IDは、プロジェクト全体で一意のIDですが、CI_MERGE_REQUEST_IIDはプロジェクト内で一意です。
CI_MERGE_REQUEST_IDは、他のプロジェクトとのマージリクエストIDと区別するために使用される一意の数値です。一方、CI_MERGE_REQUEST_IIDは、プロジェクト内のマージリクエストの相対的な識別子です。
CI_MERGE_REQUEST_IDは数値の形式を取りますが、CI_MERGE_REQUEST_IIDは文字列の形式を取ります。
したがって、CI_MERGE_REQUEST_IDは他のプロジェクトとのマージリクエストの一意性を確保するために使用され、CI_MERGE_REQUEST_IIDは特定のプロジェクト内のマージリクエストを相対的に識別するために使用されます。

なるほどな。

というわけでコードを直した。

-merge_request_id = os.environ.get('CI_MERGE_REQUEST_ID')
+merge_request_id = os.environ.get('CI_MERGE_REQUEST_IID')

最終的に下記のように修正

import requests
import os

# テストととしてこのコメントを書いてみます。

# GitLabのURL、アクセストークン、プロジェクトID、マージリクエストのIIDを指定します
gitlab_url = 'http://192.168.0.20'
access_token = os.environ.get('PERSONAL_ACCESS_TOKEN')
project_id = os.environ.get('CI_PROJECT_ID')
merge_request_iid = os.environ.get('CI_MERGE_REQUEST_IID')

print(access_token)
print(project_id)
print(merge_request_iid)

# リクエストヘッダーにアクセストークンを設定します
headers = {'PRIVATE-TOKEN': access_token}

# マージリクエストの変更内容を取得するAPIエンドポイントを構築します
api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'

print(api_url)

# APIリクエストを送信してマージリクエストの変更内容を取得します
response = requests.get(api_url, headers=headers)

# レスポンスのJSONデータから変更内容を抽出します
changes = response.json()

# 変更内容を表示します
print(changes)

↓ いけた!

http://192.168.0.20/api/v4/projects/3/merge_requests/3/changes
{'id': 5, 'iid': 3, 'project_id': 3, 'title': 'Refactor mr review', 'description': '', 'state': 'opened', 'created_at': '2023-06-18T14:10:12.610Z', 'updated_at': '2023-06-18T14:40:04.736Z', 'merged_by': None, 'merge_user': None, 'merged_at': None, 'closed_by': None, 'closed_at': None, 'target_branch': 'main', 'source_branch': 'refactor-mr-review', 'user_notes_count': 0, 'upvotes': 0, 'downvotes': 0, 'author': {'id': 1, 'username': 'root', 'name': 'Administrator', 'state': 'active', 'avatar_url': 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', 'web_url': 'http://gitlab.example.com/root'}, 'assignees': [], 'assignee': None, 'reviewers': [], 'source_project_id': 3, 'target_project_id': 3, 'labels': [], 'draft': False, 'work_in_progress': False, 'milestone': None, 'merge_when_pipeline_succeeds': False, 'merge_status': 'can_be_merged', 'detailed_merge_status': 'mergeable', 'sha': '0d9f59177390bc8c4c96f85800b136cf4775f053', 'merge_commit_sha': None, 'squash_commit_sha': None, 'discussion_locked': None, 'should_remove_source_branch': None, 'force_remove_source_branch': True, 'reference': '!3', 'references': {'short': '!3', 'relative': '!3', 'full': 'root/mr-review-test!3'}, 'web_url': 'http://gitlab.example.com/root/mr-review-test/-/merge_requests/3', 'time_stats': {'time_estimate': 0, 'total_time_spent': 0, 'human_time_estimate': None, 'human_total_time_spent': None}, 'squash': False, 'squash_on_merge': False, 'task_completion_status': {'count': 0, 'completed_count': 0}, 'has_conflicts': False, 'blocking_discussions_resolved': True, 'subscribed': True, 'changes_count': '2', 'latest_build_started_at': '2023-06-18T14:40:59.771Z', 'latest_build_finished_at': None, 'first_deployed_to_production_at': None, 'pipeline': {'id': 49, 'iid': 34, 'project_id': 3, 'sha': '0d9f59177390bc8c4c96f85800b136cf4775f053', 'ref': 'refs/merge-requests/3/head', 'status': 'running', 'source': 'merge_request_event', 'created_at': '2023-06-18T14:40:20.895Z', 'updated_at': '2023-06-18T14:40:59.776Z', 'web_url': 'http://gitlab.example.com/root/mr-review-test/-/pipelines/49'}, 'head_pipeline': {'id': 49, 'iid': 34, 'project_id': 3, 'sha': '0d9f59177390bc8c4c96f85800b136cf4775f053', 'ref': 'refs/merge-requests/3/head', 'status': 'running', 'source': 'merge_request_event', 'created_at': '2023-06-18T14:40:20.895Z', 'updated_at': '2023-06-18T14:40:59.776Z', 'web_url': 'http://gitlab.example.com/root/mr-review-test/-/pipelines/49', 'before_sha': '0000000000000000000000000000000000000000', 'tag': False, 'yaml_errors': None, 'user': {'id': 1, 'username': 'root', 'name': 'Administrator', 'state': 'active', 'avatar_url': 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', 'web_url': 'http://gitlab.example.com/root'}, 'started_at': '2023-06-18T14:40:59.771Z', 'finished_at': None, 'committed_at': None, 'duration': None, 'queued_duration': 38, 'coverage': None, 'detailed_status': {'icon': 'status_running', 'text': 'running', 'label': 'running', 'group': 'running', 'tooltip': 'running', 'has_details': True, 'details_path': '/root/mr-review-test/-/pipelines/49', 'illustration': None, 'favicon': '/assets/ci_favicons/favicon_status_running-9c635b2419a8e1ec991c993061b89cc5aefc0743bb238ecd0c381e7741a70e8c.png'}}, 'diff_refs': {'base_sha': '55d189ed3b876d106f75d16f247a5c30b154aaea', 'head_sha': '0d9f59177390bc8c4c96f85800b136cf4775f053', 'start_sha': '55d189ed3b876d106f75d16f247a5c30b154aaea'}, 'merge_error': None, 'user': {'can_merge': True}, 'changes': [{'diff': '@@ -1,3 +1,9 @@\n+before_script:\n+  - python -V\n+  - pip install virtualenv\n+  - virtualenv venv\n+  - source venv/bin/activate\n+\n build:\n   script:\n     - echo Hello,World\n@@ -8,4 +14,6 @@ mr-review-gpt:\n     - pip install -r requirements.txt\n     - python mr_review.py\n   rules:\n-    - if: \'$CI_PIPELINE_SOURCE == "merge_request_event"\'\n\\ No newline at end of file\n+    - if: \'$CI_PIPELINE_SOURCE == "merge_request_event"\'\n+  # - if: $CI_MERGE_REQUEST_ID \n+  # (https://gitlab-docs.creationline.com/ee/ci/parent_child_pipelines.html) でもOK\n\\ No newline at end of file\n', 'new_path': '.gitlab-ci.yml', 'old_path': '.gitlab-ci.yml', 'a_mode': '100644', 'b_mode': '100644', 'new_file': False, 'renamed_file': False, 'deleted_file': False}, {'diff': "@@ -1,11 +1,17 @@\n import requests\n import os\n \n+# テストととしてこのコメントを書いてみます。\n+\n # GitLabのURL、アクセストークン、プロジェクトID、マージリクエストのIIDを指定します\n-gitlab_url = 'http://gitlab.example.com'\n+gitlab_url = 'http://192.168.0.20'\n access_token = os.environ.get('PERSONAL_ACCESS_TOKEN')\n project_id = os.environ.get('CI_PROJECT_ID')\n-merge_request_id = os.environ.get('CI_MERGE_REQUEST_ID')\n+merge_request_iid = os.environ.get('CI_MERGE_REQUEST_IID')\n+\n+print(access_token)\n+print(project_id)\n+print(merge_request_iid)\n \n # リクエストヘッダーにアクセストークンを設定します\n headers = {'PRIVATE-TOKEN': access_token}\n@@ -13,6 +19,8 @@ headers = {'PRIVATE-TOKEN': access_token}\n # マージリクエストの変更内容を取得するAPIエンドポイントを構築します\n api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'\n \n+print(api_url)\n+\n # APIリクエストを送信してマージリクエストの変更内容を取得します\n response = requests.get(api_url, headers=headers)\n \n", 'new_path': 'mr_review.py', 'old_path': 'mr_review.py', 'a_mode': '100644', 'b_mode': '100644', 'new_file': False, 'renamed_file': False, 'deleted_file': False}], 'overflow': False}
Job succeeded
merge-requests-job_success

よしよし。
次は、この変更を ChatGPT に食わせることをやってみる。

変更を送信して ChatGPT にレビューしてもらう。

import openai
import requests
import os

# GitLabのURL、アクセストークン、プロジェクトID、マージリクエストのIIDを指定します
gitlab_url = 'http://192.168.0.20'
access_token = os.environ.get('PERSONAL_ACCESS_TOKEN')
project_id = os.environ.get('CI_PROJECT_ID')
merge_request_iid = os.environ.get('CI_MERGE_REQUEST_IID')

# OpenAIのAPI_KEYとModelを指定します
openai.api_key = os.environ.get("OPENAI_API_KEY")
model = 'gpt-3.5-turbo'

# リクエストヘッダーにアクセストークンを設定します
headers = {'PRIVATE-TOKEN': access_token}

# マージリクエストの変更内容を取得するAPIエンドポイントを構築します
gitlab_api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'

# APIリクエストを送信してマージリクエストの変更内容を取得します
mr_changes_response = requests.get(gitlab_api_url, headers=headers)

# レスポンスのJSONデータから変更内容を抽出します
mr_changes_data  = mr_changes_response.json()

code_changes = mr_changes_data['changes']

response = openai.Completion.create(
        engine=model,
        prompt = f"次のコード変更を分析し、修正が必要な問題がある場合は見つけます。コードの変更は git diff 表記で行われ、- で始まる行が削除され、+ で始まる行が追加されます。" +
                 f": {code_changes}",
        max_tokens=500,
)

analysis = f"This comment is auto generated by OpenAI {model} : {response['choices'][0]['text']} "

# Make comment on merge request with the analysis
comment_url = f"{gitlab_url}/projects/{project_id}/merge_requests/{merge_request_iid}/notes"
comment_data = {
    "body": analysis
}
comment_response = requests.post(comment_url, headers=headers, data=comment_data)

print(comment_response)

コードを変更した。

あとは、GitLab の CI/CD に OpenAI の APIKEY を設定する。

openai.error.AuthenticationError: No API key provided. You can set your API key in code using 'openai.api_key = <API-KEY>', or you can set the environment variable OPENAI_API_KEY=<API-KEY>). If your API key is stored in a file, you can point the openai module at it with 'openai.api_key_path = <PATH>'. You can generate API keys in the OpenAI web interface. See https://platform.openai.com/account/api-keys for details.

なんかエラーでた。

  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 298, in request
    resp, got_stream = self._interpret_response(result, stream)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 700, in _interpret_response
    self._interpret_response_line(
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 763, in _interpret_response_line
    raise self.handle_error_response(
openai.error.RateLimitError: You exceeded your current quota, please check your plan and billing details.

openai.error.RateLimitError: You exceeded your current quota, please check your plan and billing details. ということなので、発行したやつを見てみる。

OpenAPIAPI_KEY にて料金を設定していないみたい。

クレジットカードの設定をしていなかったのでした。
Usage Limit$10.00 に設定。

これでもう一回 Job を実行してみる。

  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_resources/completion.py", line 25, in create
    return super().create(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_resources/abstract/engine_api_resource.py", line 153, in create
    response, _, api_key = requestor.request(
                           ^^^^^^^^^^^^^^^^^^
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 298, in request
    resp, got_stream = self._interpret_response(result, stream)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 700, in _interpret_response
    self._interpret_response_line(
  File "/builds/root/mr-review-test/venv/lib/python3.11/site-packages/openai/api_requestor.py", line 763, in _interpret_response_line
    raise self.handle_error_response(
openai.error.InvalidRequestError: This is a chat model and not supported in the v1/completions endpoint. Did you mean to use v1/chat/completions?

エラーが出た・・・。

CompletionsAPIのコールの仕方がこれではないらしい。

import openai
import requests
import os

# GitLabのURL、アクセストークン、プロジェクトID、マージリクエストのIIDを指定します
gitlab_url = 'http://192.168.0.20'
access_token = os.environ.get('PERSONAL_ACCESS_TOKEN')
project_id = os.environ.get('CI_PROJECT_ID')
merge_request_iid = os.environ.get('CI_MERGE_REQUEST_IID')

# OpenAIのAPI_KEYとModelを指定します
openai.api_key = os.environ.get('OPENAI_API_KEY')
model = 'gpt-3.5-turbo'

# リクエストヘッダーにアクセストークンを設定します
headers = {'PRIVATE-TOKEN': access_token}

# マージリクエストの変更内容を取得するAPIエンドポイントを構築します
gitlab_api_url = f'{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/changes'

# APIリクエストを送信してマージリクエストの変更内容を取得します
mr_changes_response = requests.get(gitlab_api_url, headers=headers)

# レスポンスのJSONデータから変更内容を抽出します
mr_changes_data  = mr_changes_response.json()

code_changes = mr_changes_data['changes']

print(code_changes)

response = openai.ChatCompletion.create(
        model=model,
        messages = [
          {"role": "system", "content": "あなたはプログラミングのスペシャリストでコードレビューをするアシスタントです。"},
          {"role": "user", "content":f"次のコード変更を分析し、修正が必要な問題がある場合は見つけます。 コードの変更は git diff 表記で行われ、- で始まる行が削除され、+ で始まる行が追加されます。" +
                 f": {code_changes}",
          }
        ],
        max_tokens=2000,
)

analysis = f"This comment is auto generated by OpenAI {model} : {response['choices'][0]['message']['content']} "

print(analysis)
# Make comment on merge request with the analysis
comment_url = f"{gitlab_url}/projects/{project_id}/merge_requests/{merge_request_iid}/notes"
comment_data = {
    "body": analysis
}
comment_response = requests.post(comment_url, headers=headers, data=comment_data)

print(comment_response)
This comment is auto generated by OpenAI gpt-3.5-turbo : このコード変更にはいくつかの問題があるようです。
まず、.gitlab-ci.yml ファイルの変更部分で、before_script で virtualenv を作成していますが、その後の script では venv をアクティブ化していません。したがって、ビルドが失敗する可能性があります。
次に、mr_review.py ファイルの変更部分で、未定義の変数 merge_request_iid を使用しています。変数名が間違っているか、定義されていない可能性があります。
また、OpenAI API キーやモデルの指定がありますが、環境変数から取得するように指定されていません。セキュリティ上の理由から、暗号化されたシークレットにアクセスするために環境変数を使用することが推奨されています。
最後に、コメントの投稿に失敗する可能性があります。コメントを投稿するためのAPIエンドポイントが正しくない可能性があるため、確認が必要です。
これらの問題を解決するために、以下の修正を行うことを提案します:
1. .gitlab-ci.yml ファイルの修正
  - script で venv をアクティブ化するように追加する
2. mr_review.py ファイルの修正 
  - merge_request_iid を merge_request_id に変更する
  - OpenAI API キーやモデルの指定を環境変数から取得するように修正する
  - コメント投稿用のAPIエンドポイントを修正する
以上の修正が必要です。 

こんなレスポンスが得られていた。
しかし、コメントには反映されず・・・。
コメントのAPIを見てみる。

/api/v4 のURLになっていなかったので修正する。

-comment_url = f"{gitlab_url}/projects/{project_id}/merge_requests/{merge_request_iid}/notes"
+comment_url = f"{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{merge_request_iid}/discussions"

再度 Job を実行する。
できた! success-comment.png

参考

改善点

  • コメントするアカウントを変更する。→ レビューコメント用のユーザを作成し、そのユーザの Personal Access Token を使えば良さそう。
  • プロジェクト間でRunner を共有したい。→Group Runner, Shared Runnerを使う。
  • 現状だと MRに変更があった場合にレビューが都度入る。→その都度コメントされてしまうため。良いレビューの仕方を考えたい。
  • ChatGPTがレビューコメントしたものに返信があった場合に、そのレビューを返したい。
  • Reviewer が特定のユーザの場合のみに ChatGPT のレビューを有効にしたい。→ .gitlab-ci.yml を変えればよい?

おわりに

作業ログという形で今回の作業のエラー解消や構築までの流れを記録しておいた。
これを基に、スムーズに構築できる記事を次回は記載する。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。