Skip to content

feat: add native GitLab MR review integration#622

Merged
anandgupta42 merged 7 commits intoAltimateAI:mainfrom
VJ-yadav:feat/gitlab-mr-review
Apr 4, 2026
Merged

feat: add native GitLab MR review integration#622
anandgupta42 merged 7 commits intoAltimateAI:mainfrom
VJ-yadav:feat/gitlab-mr-review

Conversation

@VJ-yadav
Copy link
Copy Markdown
Contributor

@VJ-yadav VJ-yadav commented Apr 3, 2026

Summary

  • Adds altimate-code gitlab review <mr-url> CLI command for reviewing GitLab merge requests
  • Fetches MR diffs via GitLab REST API v4, runs AI code review, and posts results as MR notes
  • Supports gitlab.com, self-hosted instances, and nested group project paths

Test Plan

  • TypeCheck passes (only pre-existing ClickHouse error)
  • 10 unit tests for URL parser covering: standard URLs, nested groups, self-hosted instances, ports, fragments, invalid inputs
  • Manual test: GITLAB_TOKEN=xxx altimate-code gitlab review https://gitlab.com/org/repo/-/merge_requests/123
  • Manual test: --no-post-comment flag to review without posting
  • Manual test: --model flag to override the AI model

Checklist

  • No new dependencies (uses Node.js built-in fetch)
  • No any types — all errors typed as unknown, branded ProviderID/ModelID types used
  • No else statements
  • No let mutations (except event subscriber accumulator, matching github.ts pattern)
  • .catch() preferred over try/catch
  • Token never logged — maskToken() helper for error messages
  • No hardcoded instance URLs — resolved from MR URL or GITLAB_INSTANCE_URL env var
  • Reuses existing extractResponseText and formatPromptTooLargeError from github.ts
  • Follows altimate_change marker convention in index.ts

Fixes #618

Summary by CodeRabbit

  • New Features
    • Added a GitLab merge request review command that streams AI review output to the console and, by default, posts or updates a single review comment on the MR. Supports self-hosted instances and various MR URL formats.
  • Tests
    • Added comprehensive tests for MR URL parsing covering gitlab.com, self-hosted instances, nested groups, fragments, ports, and invalid inputs.

Adds `altimate-code gitlab review <mr-url>` CLI command that fetches MR
diffs and posts AI review comments back to GitLab merge requests.

- Parses GitLab MR URLs (any instance, nested groups, self-hosted)
- Fetches MR metadata, diffs, and existing comments via REST API v4
- Runs AI code review using the existing session/prompt infrastructure
- Posts review results as MR notes with deduplication marker
- Exports discussion API helper for future inline commenting
- Auth via GITLAB_PERSONAL_ACCESS_TOKEN or GITLAB_TOKEN env vars
- Self-hosted instances via GITLAB_INSTANCE_URL or auto-detected from URL

Fixes AltimateAI#618

Co-Authored-By: Vijay Yadav <vjyadav194@gmail.com>
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: eba2ae4b-4934-4173-a877-52326c6c78b5

📥 Commits

Reviewing files that changed from the base of the PR and between b9e83dd and 3997027.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/gitlab.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/opencode/src/cli/cmd/gitlab.ts

📝 Walkthrough

Walkthrough

Added a new GitLab MR review CLI: parses MR URLs, fetches MR metadata/changes/notes via GitLab API, constructs an AI review prompt, streams model output, and optionally posts or updates the review as a GitLab MR note. Includes unit tests for MR URL parsing.

Changes

Cohort / File(s) Summary
GitLab Command Implementation
packages/opencode/src/cli/cmd/gitlab.ts
New CLI command and handler gitlab review; parseGitLabMRUrl export; GitLab REST helpers (token auth), fetch MR metadata/changes/notes, create/update notes; prompt construction, Session orchestration, Bus-based streaming; re-exports fetchMRMetadata, fetchMRChanges, fetchMRNotes, postMRNote, updateMRNote.
CLI Registration
packages/opencode/src/index.ts
Imports and registers GitlabCommand as a top-level CLI subcommand.
Tests
packages/opencode/test/cli/gitlab-mr-url.test.ts
Adds Bun tests for parseGitLabMRUrl covering gitlab.com and self-hosted MR URLs, nested groups, fragments, HTTP/ports, and multiple invalid/non-MR cases returning null.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Cmd as GitLab Command
    participant API as GitLab API
    participant AI as AI Model
    participant Bus as Event Bus

    User->>Cmd: gitlab review <mr-url>
    Cmd->>Cmd: parseGitLabMRUrl(url)
    Cmd->>API: GET /projects/:id/merge_requests/:iid (metadata)
    API-->>Cmd: metadata
    Cmd->>API: GET /projects/:id/merge_requests/:iid/changes
    API-->>Cmd: diffs
    Cmd->>API: GET /projects/:id/merge_requests/:iid/notes
    API-->>Cmd: notes
    Cmd->>AI: Build prompt (metadata + diffs + notes) and start Session
    AI->>Bus: stream parts/events
    Bus-->>Cmd: session-part events
    Cmd->>User: stream output to terminal
    alt post-comment enabled
        Cmd->>API: POST/PUT /projects/:id/merge_requests/:iid/notes
        API-->>Cmd: note created/updated
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

contributor

Poem

🐇 I nibbled URLs and chased each diff,
I stitched prompts from patches, clever and swift.
I fetched old notes and left new lines—
A hopping review in tidy outlines.
🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding native GitLab MR review integration. It is specific, directly related to the changeset, and communicates the primary objective.
Description check ✅ Passed The PR description includes all required template sections: Summary (with what changed and why), Test Plan (with specific tests listed), and Checklist (with implementation constraints verified). The description is comprehensive and complete.
Linked Issues check ✅ Passed The PR implementation meets the primary coding requirements from issue #618: fetches MR diffs via GitLab REST API v4 [#618], posts AI review results as MR notes [#618], supports gitlab.com and self-hosted instances [#618], and uses required endpoints like GET /merge_requests/:mr_iid/changes [#618].
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing the GitLab MR review feature: new gitlab.ts CLI command module, CLI registration in index.ts, and comprehensive unit tests for URL parsing. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/cli/cmd/gitlab.ts`:
- Around line 374-385: The final assistant output is being printed twice because
subscribeSessionEvents(session) renders the completed review when a part
finishes (part.time?.end) and then the code prints reviewText after runReview()
resolves; modify the post-run printing to avoid duplication by removing or
guarding the UI.println(UI.markdown(reviewText)) step: either have runReview
return undefined when subscribeSessionEvents handles final rendering or add a
conditional check before calling UI.println to only print when
subscribeSessionEvents did not already render the final assistant output; apply
the same change for the similar block around the runReview call at the other
location (the block currently at lines shown also containing
UI.println/UI.markdown usage).
- Around line 195-208: The code defines postMRDiscussion(instanceUrl, projectId,
mrIid, token, body, position) which posts a line-level discussion but is
exported and never invoked, so the current flow always creates a single MR note;
fix by wiring this function into the MR feedback path: where MR notes are
created (the function(s) that currently call gitlabApi to post a single note),
detect when a finding includes file/line information, map that finding to a
GitLab DiscussionPosition and call postMRDiscussion instead of the single-note
API; keep the existing single-note behavior for non-positional feedback and
ensure the exported postMRDiscussion is actually imported/used in the module
that posts MR feedback (update the calling code that currently posts via
gitlabApi to delegate to postMRDiscussion when position is present).
- Around line 225-227: The code filters marker notes from the prompt but still
always creates a new marked review comment, causing duplicate posts; modify the
logic that posts the full-review comment to first scan the existing notes array
for a note whose body starts with "<!-- altimate-code-review -->" (use the same
notes variable/filter as in noteLines), and if found either update that existing
note (using its id) instead of creating a new one or skip posting a new comment
entirely; apply the same change for the other duplicate-creating sites
referenced (the blocks around lines mentioned), ensuring all places that
currently always create a fresh marked note instead update/skip when an existing
marker is present.
- Around line 317-320: The handler is calling process.exit(1) directly after
UI.error (using mrUrl and parsed), which bypasses top-level finally/cleanup
(e.g., Telemetry.shutdown); instead remove process.exit(1) and propagate the
failure by throwing an error (e.g., throw new Error("Invalid GitLab MR URL")
including mrUrl) or return a rejected Promise so the top-level runner can handle
exit and run cleanup. Apply the same change at the other occurrences noted (the
blocks around lines with UI.error and process.exit at the other locations).
- Around line 323-328: The code always prefers parsed.instanceUrl from
parseGitLabMRUrl(), making resolveInstanceUrl()/GITLAB_INSTANCE_URL ineffective;
update the logic in the gitlab command so the environment value can override the
parsed MR URL when present (or explicitly treat the env as a fallthrough):
change the instanceUrl assignment to prefer envInstanceUrl when non-empty (e.g.
instanceUrl = envInstanceUrl || parsed.instanceUrl) or add a documented
flag/parameter to force override; adjust any downstream use of instanceUrl
accordingly and keep resolveInstanceUrl() usage consistent with the chosen
precedence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76cda296-dd65-4f24-bb35-dd88a4ef8136

📥 Commits

Reviewing files that changed from the base of the PR and between 0d34855 and bdabf46.

📒 Files selected for processing (3)
  • packages/opencode/src/cli/cmd/gitlab.ts
  • packages/opencode/src/index.ts
  • packages/opencode/test/cli/gitlab-mr-url.test.ts

Comment on lines +195 to +208
async function postMRDiscussion(
instanceUrl: string,
projectId: string,
mrIid: number,
token: string,
body: string,
position: DiscussionPosition,
): Promise<unknown> {
return gitlabApi(
instanceUrl,
`/projects/${projectId}/merge_requests/${mrIid}/discussions`,
token,
{ method: "POST", body: { body, position } },
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The inline-review half of the feature is still not wired up.

This path always posts a single MR note. postMRDiscussion() is exported, but never used, so the line-level feedback called out in issue #618 still cannot reach GitLab discussions.

If you want, I can help sketch the finding-to-position mapping as a follow-up.

Also applies to: 388-396, 501-502

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/gitlab.ts` around lines 195 - 208, The code
defines postMRDiscussion(instanceUrl, projectId, mrIid, token, body, position)
which posts a line-level discussion but is exported and never invoked, so the
current flow always creates a single MR note; fix by wiring this function into
the MR feedback path: where MR notes are created (the function(s) that currently
call gitlabApi to post a single note), detect when a finding includes file/line
information, map that finding to a GitLab DiscussionPosition and call
postMRDiscussion instead of the single-note API; keep the existing single-note
behavior for non-positional feedback and ensure the exported postMRDiscussion is
actually imported/used in the module that posts MR feedback (update the calling
code that currently posts via gitlabApi to delegate to postMRDiscussion when
position is present).

@dev-punia-altimate
Copy link
Copy Markdown

❌ Tests — Failures Detected

TypeScript — 15 failure(s)

  • connection_refused [3.06ms]
  • timeout [2.84ms]
  • permission_denied [2.94ms]
  • parse_error [2.68ms]
  • oom [2.95ms]
  • network_error [2.74ms]
  • auth_failure [2.95ms]
  • rate_limit [3.14ms]
  • internal_error [3.20ms]
  • empty_error [0.29ms]
  • connection_refused [0.19ms]
  • timeout [0.09ms]
  • permission_denied [0.09ms]
  • parse_error [0.09ms]
  • oom [0.14ms]

cc @VJ-yadav
Tested at bdabf46b | Run log | Powered by QA Autopilot

@anandgupta42
Copy link
Copy Markdown
Contributor

@VJ-yadav Can you share screenshot of how you tested the feature?

- Replace process.exit(1) with thrown errors so top-level finally
  block runs Telemetry.shutdown() on failure (matches github.ts pattern)
- Deduplicate review comments: detect existing marker note and update
  via PUT instead of always creating a new one
- Remove duplicate output printing (subscribeSessionEvents already
  renders the completed assistant message)
- Fix GITLAB_INSTANCE_URL env var precedence: env now overrides the
  URL parsed from the MR link, enabling self-hosted proxy/mirror use
- Remove unused postMRDiscussion/DiscussionPosition dead code; add
  updateMRNote helper used by the dedup logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/opencode/src/cli/cmd/gitlab.ts (1)

364-377: ⚠️ Potential issue | 🟠 Major

Inline discussion posting is still not wired into the MR feedback path.

Lines 365-377 only create/update a general note. The linked objective for line-level comments via GitLab discussions endpoint is still unmet.

If you want, I can draft a follow-up that maps structured findings with file/line metadata into /discussions positions, while keeping summary text in the marker note.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/gitlab.ts` around lines 364 - 377, The current
block only creates/updates a single MR note (commentBody) and never posts
file/line inline discussions; extend this path to also create/update GitLab
discussions for structured findings by mapping each finding's file/path and
line/position into the GitLab discussions API. After computing commentBody as
before, iterate the parsed structured findings (the array/structure you produce
when generating reviewText) and call a new helper (e.g., postMRDiscussion or
updateMRDiscussion) that uses the GitLab
/projects/:id/merge_requests/:iid/discussions endpoint for each finding; keep
the existing summary note logic (updateMRNote/postMRNote) to store the marker
comment, but add calls to create per-file inline discussions using the same
token/instanceUrl/projectId/mrIid and the finding's file and line metadata so
reviewers see line-level comments in the MR.
🧹 Nitpick comments (1)
packages/opencode/src/cli/cmd/gitlab.ts (1)

358-363: Return and clean up the Bus subscription.

Line 454 subscribes without keeping an unsubscribe handle. In repeated invocations within the same process (tests/embedded usage), listeners can accumulate and duplicate output.

♻️ Proposed refactor
-      subscribeSessionEvents(session)
+      const unsubscribe = subscribeSessionEvents(session)

-      const reviewText = await runReview(session.id, variant, providerID, modelID, reviewPrompt)
+      const reviewText = await runReview(session.id, variant, providerID, modelID, reviewPrompt).finally(() =>
+        unsubscribe(),
+      )

-function subscribeSessionEvents(session: { id: SessionID; title: string; version: string }) {
+function subscribeSessionEvents(session: { id: SessionID; title: string; version: string }) {
@@
-  Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
+  const unsubscribe = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
@@
-  })
+  })
+  return unsubscribe
 }

Also applies to: 454-479

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/gitlab.ts` around lines 358 - 363, The
subscription created by subscribeSessionEvents in gitlab.ts is never
unsubscribed, causing listener accumulation on repeated runs; modify the call in
the function that calls subscribeSessionEvents (the same scope that awaits
runReview) to capture its unsubscribe/cleanup handle (e.g., const unsubscribe =
subscribeSessionEvents(session) or similar return value) and ensure you call
that unsubscribe/cleanup after runReview completes or on early exits/errors (use
finally or equivalent). Update any other places noted (the 454–479 block) that
call subscribeSessionEvents to follow the same pattern so listeners are removed
when done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/opencode/src/cli/cmd/gitlab.ts`:
- Around line 164-165: The MR notes fetch is limited to a single page of 100
(`/projects/${projectId}/merge_requests/${mrIid}/notes?sort=asc&per_page=100`)
so the dedup/update code later misses marker notes on subsequent pages; change
the notes retrieval to paginate until all pages are retrieved (follow GitLab
pagination via page/next-page or X-Next-Page header) and return a concatenated
list used by the dedup/update logic (identify the fetch call that uses token and
the dedup/update block that checks existing notes, e.g., the function that
constructs that URL and the code around the dedupe check) so the marker note is
found regardless of page.
- Around line 460-463: The current assignment to title incorrectly groups the ||
with the ternary, causing part.state.title to be ignored; update the expression
used when computing title (the const title variable) so the ternary binds
correctly — e.g., ensure you use parentheses so it reads: part.state.title ||
(Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) :
"Unknown"), referencing part.state.title and part.state.input to determine the
displayed title.
- Around line 108-112: The fetch call in gitlab.ts (where `const res = await
fetch(url, {...})` is used by fetchMRMetadata, fetchMRChanges, and fetchMRNotes)
must use an AbortController to enforce a timeout: create an AbortController,
pass its signal to fetch, set a timer (e.g., configurable default like 10s) that
calls controller.abort(), and clear the timer after fetch completes; update the
surrounding function to handle an aborted request (detect AbortError or
response.ok false) and surface a clear timeout error. Ensure the timer is
cleaned up on success or failure and make the timeout value configurable via an
option or constant used by those callers.

---

Duplicate comments:
In `@packages/opencode/src/cli/cmd/gitlab.ts`:
- Around line 364-377: The current block only creates/updates a single MR note
(commentBody) and never posts file/line inline discussions; extend this path to
also create/update GitLab discussions for structured findings by mapping each
finding's file/path and line/position into the GitLab discussions API. After
computing commentBody as before, iterate the parsed structured findings (the
array/structure you produce when generating reviewText) and call a new helper
(e.g., postMRDiscussion or updateMRDiscussion) that uses the GitLab
/projects/:id/merge_requests/:iid/discussions endpoint for each finding; keep
the existing summary note logic (updateMRNote/postMRNote) to store the marker
comment, but add calls to create per-file inline discussions using the same
token/instanceUrl/projectId/mrIid and the finding's file and line metadata so
reviewers see line-level comments in the MR.

---

Nitpick comments:
In `@packages/opencode/src/cli/cmd/gitlab.ts`:
- Around line 358-363: The subscription created by subscribeSessionEvents in
gitlab.ts is never unsubscribed, causing listener accumulation on repeated runs;
modify the call in the function that calls subscribeSessionEvents (the same
scope that awaits runReview) to capture its unsubscribe/cleanup handle (e.g.,
const unsubscribe = subscribeSessionEvents(session) or similar return value) and
ensure you call that unsubscribe/cleanup after runReview completes or on early
exits/errors (use finally or equivalent). Update any other places noted (the
454–479 block) that call subscribeSessionEvents to follow the same pattern so
listeners are removed when done.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a028bb8a-513a-4c10-a77d-fa6baa4bc208

📥 Commits

Reviewing files that changed from the base of the PR and between bdabf46 and b9e83dd.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/gitlab.ts

@VJ-yadav
Copy link
Copy Markdown
Contributor Author

VJ-yadav commented Apr 4, 2026

@VJ-yadav Can you share screenshot of how you tested the feature?

Tested against a GitLab MR containing a Python file with intentional security issues (SQL injection, command injection, hardcoded secrets, eval(), path traversal).

SETUP: altimate-code gitlab review "https://gitlab.com/altimatecode/mr-review-test/-/merge_requests/1 --model opencode/big-pickle"

  1. Terminal output (CLI fetches MR, runs AI review, posts comment):
Screenshot 2026-04-03 at 7 58 34 PM
  1. GitLab MR showing the AI-generated review:
Screenshot 2026-04-03 at 7 54 01 PM

The AI caught all intentional issues: hardcoded credentials, SQL injection, command injection via eval()/os.system()/subprocess.call(shell=True), and path traversal.

Review is posted with a marker for deduplication.

Additional behavior verified:

Re-running the command updates the existing review note instead of creating duplicates
--no-post-comment flag prints review to terminal only
Self-hosted GitLab instances supported via GITLAB_INSTANCE_URL env var

@anandgupta42

VJ-yadav and others added 5 commits April 3, 2026 20:10
- Add 30s timeout to gitlabApi fetch via AbortController to prevent
  indefinite hangs on network stalls
- Paginate MR notes (loop pages of 100) so dedup marker detection
  works on active MRs with >100 notes
- Fix ternary precedence in tool title rendering — part.state.title
  was being ignored due to || binding with the ternary condition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@anandgupta42 anandgupta42 merged commit 453e7e1 into AltimateAI:main Apr 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native GitLab MR review integration — fetch diff and post review comments

3 participants