Skip to content

feat(gitlab): batch-publish committable code suggestions as one review - #2635

Merged
IsmaelMartinez merged 5 commits into
The-PR-Agent:mainfrom
nlinakis-xm:feature/gitlab-batch-publish-code-suggestions
Aug 26, 2026
Merged

feat(gitlab): batch-publish committable code suggestions as one review#2635
IsmaelMartinez merged 5 commits into
The-PR-Agent:mainfrom
nlinakis-xm:feature/gitlab-batch-publish-code-suggestions

Conversation

@nlinakis-xm

@nlinakis-xm nlinakis-xm commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

commitable_code_suggestions on GitLab posts each suggestion as its own live discussion the moment it's created, so every suggestion fires its own notification (and email, if configured). GitHub already avoids this: GithubProvider.publish_code_suggestions() batches everything into a single create_review(comments=[...]) call, which is exactly what #361 asked for and fixed - but only on the GitHub side. GitLab never got an equivalent.

This adds an opt-in gitlab.publish_code_suggestions_as_review setting (default false, so existing behavior is unchanged). When enabled, GitLabProvider.publish_code_suggestions():

  • queues each suggestion as a GitLab draft note (mr.draft_notes.create(...), already supported by the pinned python-gitlab==8.3.0) instead of an immediate live discussion,
  • then publishes them all together with a single mr.draft_notes.bulk_publish() call once the loop finishes - the same mechanism GitLab's own "start a review" UI flow uses.

Suggestions remain fully committable either way; this only changes delivery timing/notification volume. The existing position-rejected fallback path (general note when a suggestion can't be anchored to a diff line) also switches to a draft note in this mode, so nothing leaks out as a live comment ahead of the batch.

One change is not behind the new flag: pr_agent/algo/inline_comment_dedup.py now also scans pending draft notes on the GitLab path. Without it, a marker sitting in a draft left over from a run whose bulk-publish failed would be invisible to the dedup scan and the suggestion would be re-posted as a duplicate once published. That path is still gated behind the existing config.persistent_inline_comments (default false).

Naming follows the existing publish_X_as_Y convention already used in this file (publish_review_as_thread) and elsewhere (publish_description_as_comment, publish_as_check_run).

I searched existing issues/PRs for prior art on this before starting - #361 is the closest (GitHub-side version of the same complaint, already fixed) - and found nothing open for the GitLab side.

Test plan

  • Added tests/unittest/test_gitlab_batch_publish_suggestions.py, 8 tests: flag off leaves behaviour unchanged, flag on queues drafts and bulk-publishes once, the position-rejected fallback uses a draft note rather than a live note, a bulk-publish failure is caught and does not propagate, a total draft failure degrades every suggestion to a live comment, an empty suggestion list does not publish, an all-failed list does not publish, and a still-pending draft's marker is seen by the dedup scan.
  • Extended tests/unittest/test_inline_comment_dedup.py to cover the new GitLab draft-note scan.
  • Full unit suite on the tree merged with current main: 2367 passed, 1 skipped, 1 xfailed.
  • Restoring the source files from main while keeping the new tests turns 5 of the 9 red, so they exercise the feature rather than the branch.
  • ruff check on the touched provider file: no new findings relative to main.
  • Manual smoke test against a real GitLab MR with commitable_code_suggestions=true and publish_code_suggestions_as_review=true

Docs

Added a "Batch-publishing committable suggestions on GitLab" section to docs/docs/tools/improve.md, next to the existing "Persistent inline comments" section.

commitable_code_suggestions posts each suggestion as its own live
GitLab discussion the moment it's created, so every suggestion fires
its own notification/email. GitHub already avoids this by batching
suggestions into a single create_review() call (see The-PR-Agent#361); GitLab had
no equivalent.

Add gitlab.publish_code_suggestions_as_review (default false): when
enabled, suggestions are queued as GitLab draft notes and published
together with a single draft_notes.bulk_publish() call, mirroring the
GitLab UI's "start a review" flow. Suggestions remain fully
committable either way - this only changes delivery timing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature 💡 label Aug 12, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

GitLab: optionally batch-publish committable suggestions via draft notes

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add opt-in GitLab mode to queue suggestions as draft notes and bulk-publish once.
• Keep existing per-suggestion live discussions as the default behavior.
• Document the setting and add unit tests for batching, fallback, and failure handling.
Diagram

graph TD
  A["Settings: publish_as_review"] --> B["GitLabProvider.publish_code_suggestions"] --> C{"as_review?"}
  C -->|"false"| D["send_inline_comment (live)"] --> E{{"GitLab API: discussions/notes"}}
  C -->|"true"| F["send_inline_comment (draft)"] --> G{{"GitLab API: draft_notes.create"}} --> H{{"GitLab API: draft_notes.bulk_publish"}}

  subgraph Legend
    direction LR
    _p["Process"] ~~~ _d{"Decision"} ~~~ _e{{"External API"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Single live discussion thread containing all suggestions
  • ➕ Avoids draft-note visibility risk if bulk publish fails
  • ➕ Still reduces notification volume to one thread
  • ➖ Likely loses “committable suggestion” UX per line (anchors/suggestion blocks may not map cleanly)
  • ➖ Harder to correlate each suggestion to a specific diff position compared to inline notes
2. Client-side batching with delayed posting of live discussions
  • ➕ No reliance on draft notes feature support/permissions
  • ➖ Still triggers one notification per discussion once posted
  • ➖ No true server-side “start a review” parity; more complex retry/partial-failure semantics

Recommendation: Using GitLab draft notes + bulk_publish is the closest equivalent to GitHub’s batched review comments and GitLab’s own “start a review” workflow, while preserving inline/committable suggestions. The main tradeoff (drafts are invisible until published) is mitigated by explicit exception logging on bulk_publish failure and by GitLab retaining pending drafts for manual publish or a later successful run.

Files changed (4) +167 / -13

Enhancement (1) +35 / -13
gitlab_provider.pyAdd draft-note batching mode for GitLab code suggestions +35/-13

Add draft-note batching mode for GitLab code suggestions

• Extends send_inline_comment() with an as_draft option to post either live discussions/notes or draft notes (including the position-rejected fallback path). Adds an opt-in publish_code_suggestions_as_review flag that queues draft notes during iteration and bulk-publishes once at the end, logging (but not raising) on bulk-publish failure.

pr_agent/git_providers/gitlab_provider.py

Tests (1) +115 / -0
test_gitlab_batch_publish_suggestions.pyUnit tests for GitLab draft batching and bulk publish +115/-0

Unit tests for GitLab draft batching and bulk publish

• Adds tests covering default live-discussion behavior, draft-queuing + single bulk publish, draft fallback on position rejection, and non-propagation of bulk_publish errors. Uses a lightweight GitLabProvider instance with mocked MR endpoints and patched settings.

tests/unittest/test_gitlab_batch_publish_suggestions.py

Documentation (1) +13 / -0
improve.mdDocument GitLab batch-publishing for committable suggestions +13/-0

Document GitLab batch-publishing for committable suggestions

• Adds a new documentation section describing why GitLab suggestions can be noisy by default and how to enable batching via gitlab.publish_code_suggestions_as_review. Clarifies that suggestions are queued as draft notes and published together, without changing committability.

docs/docs/tools/improve.md

Other (1) +4 / -0
configuration.tomlIntroduce gitlab.publish_code_suggestions_as_review setting +4/-0

Introduce gitlab.publish_code_suggestions_as_review setting

• Adds a documented GitLab configuration flag (default false) to batch committable code suggestions as draft notes and publish them in one bulk call. Keeps existing behavior unchanged unless explicitly enabled.

pr_agent/settings/configuration.toml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (2) 📜 Skill insights (0)

Grey Divider


Action required

1. Draft failure drops suggestions ✓ Resolved 🐞 Bug ☼ Reliability
Description
When as_draft=True, send_inline_comment() uses mr.draft_notes.create() for both the primary
inline comment and the fallback general note, and never falls back to the previous live
discussions.create()/notes.create() behavior if draft-note creation fails. If the draft-notes
endpoint is unsupported/unavailable or consistently errors (e.g., 403/404), batch mode can fail to
post suggestions at all.
Code

pr_agent/git_providers/gitlab_provider.py[R645-648]

+                if as_draft:
+                    self.mr.draft_notes.create({'note': body, 'position': pos_obj})
+                else:
+                    self.mr.discussions.create({'body': body, 'position': pos_obj})
Evidence
The modified code paths show that when as_draft is enabled, both the primary and fallback comment
creation paths use mr.draft_notes.create(), and there is no code path that switches to live
notes/discussions on draft-note failures.

pr_agent/git_providers/gitlab_provider.py[643-717]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In batch mode, `send_inline_comment(..., as_draft=True)` posts via `mr.draft_notes.create()` in both the primary (inline) and fallback (general note) paths. If draft-note creation fails due to environment/API constraints, the code retries another draft-note creation in the fallback path and ultimately returns `False` without attempting to post a live discussion/note. This can cause suggestions to be silently dropped for users who enable batch mode but cannot use draft notes.
### Issue Context
The pre-PR behavior (`as_draft=False`) used `mr.discussions.create()` for inline and `mr.notes.create()` for fallback. Batch mode should degrade gracefully back to that behavior when draft notes are not usable.
### Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[643-717]
### Suggested fix approach
- When `as_draft=True` and `mr.draft_notes.create(...)` raises, detect “drafts unavailable” style failures (or conservatively any exception) and retry using the live endpoints:
- Primary retry: `mr.discussions.create({'body': body, 'position': pos_obj})`
- Fallback retry: `mr.notes.create({'body': body_fallback, 'position': fallback_position})`
- Ensure `publish_code_suggestions()` does not call `bulk_publish()` if the run fell back to live comments (or track draft-vs-live separately).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. bulk_publish failure returns True 📎 Requirement gap ☼ Reliability
Description
When mr.draft_notes.bulk_publish() fails, publish_code_suggestions() still returns True,
preventing the caller from triggering the per-suggestion fallback publish path. This can leave
GitLab suggestions stuck as unpublished drafts (invisible to others) while the system reports
success.
Code

pr_agent/git_providers/gitlab_provider.py[R775-778]

+        if as_review:
+            try:
+                self.mr.draft_notes.bulk_publish()
+            except Exception as e:
Evidence
PR Compliance ID 7 requires falling back to per-suggestion publishing only when the batched publish
fails; however, the new GitLab batching path swallows bulk_publish() failures and still reports
success, so the fallback in pr_code_suggestions.py never runs. This also violates PR Compliance ID
3 because the bulk-publish error is effectively ignored from the caller’s perspective (success is
returned even though publishing did not complete).

Only fall back to per-suggestion publishing if the batched publish fails
Rule 3: Robust Error Handling
pr_agent/git_providers/gitlab_provider.py[775-790]
pr_agent/tools/pr_code_suggestions.py[597-602]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GitLabProvider.publish_code_suggestions()` catches `draft_notes.bulk_publish()` exceptions but still returns `True`, which prevents upstream fallback logic from publishing suggestions individually.
## Issue Context
`pr_agent/tools/pr_code_suggestions.py` relies on the boolean return value to decide whether to retry by publishing each suggestion separately.
## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[775-790]
- pr_agent/tools/pr_code_suggestions.py[597-602]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bulk-publishes stale drafts ✓ Resolved 🐞 Bug ≡ Correctness
Description
GitLabProvider.publish_code_suggestions() calls mr.draft_notes.bulk_publish() whenever
gitlab.publish_code_suggestions_as_review is enabled, even if this invocation queued zero draft
notes. If publish_code_suggestions() is called with an empty list (e.g., all suggestions failed to
parse), this can unexpectedly publish unrelated pending draft notes already on the MR for the bot
user.
Code

pr_agent/git_providers/gitlab_provider.py[R775-777]

+        if as_review:
+            try:
+                self.mr.draft_notes.bulk_publish()
Evidence
The code unconditionally bulk-publishes whenever the flag is enabled, and the caller can invoke
publish_code_suggestions with an empty list, which would still trigger bulk_publish and potentially
publish pre-existing pending drafts.

pr_agent/git_providers/gitlab_provider.py[732-788]
pr_agent/tools/pr_code_suggestions.py[572-602]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `gitlab.publish_code_suggestions_as_review` is enabled, `publish_code_suggestions()` always calls `mr.draft_notes.bulk_publish()` even if it created no draft notes in this call. This can publish unrelated pending drafts on the MR (e.g., from a previous failed run or manual drafts by the same bot user) when the input list is empty.
### Issue Context
`pr_code_suggestions.push_inline_code_suggestions()` can call `publish_code_suggestions(code_suggestions)` even when `code_suggestions` is empty (e.g., parsing failures in the per-suggestion `try` block). In review-mode, that empty call still reaches the `bulk_publish()` call.
### Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[732-789]
### Suggested fix
- Track whether *this invocation* successfully created at least one draft note (primary or fallback path).
- Only call `bulk_publish()` if `as_review` is true **and** `drafts_created_count > 0` (or `created_any_drafts` boolean).
- Optionally also short-circuit early when `not code_suggestions` to avoid any publish-side effects.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Commented-out debug log remains 📘 Rule violation ⚙ Maintainability ⭐ New
Description
A commented-out get_logger().debug(...) block was added in the new fallback path, which is dead
code and adds maintenance noise. This violates the requirement to avoid commented-out code in
submitted changes.
Code

pr_agent/git_providers/gitlab_provider.py[R732-733]

+                # get_logger().debug(
+                #     f"Failed to create comment in MR {self.id_mr} with position {pos_obj} (probably not a '+' line)")
Evidence
PR Compliance ID 2 prohibits commented-out/dead code. The added commented-out debug lines in
gitlab_provider.py are inactive and should be removed or restored as active logging.

Rule 2: No Dead or Commented-Out Code
pr_agent/git_providers/gitlab_provider.py[732-733]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A commented-out `get_logger().debug(...)` block was introduced, which is dead code and should not be kept in the codebase.

## Issue Context
This appears to be leftover debugging/diagnostic code in `_create_suggestion_note()` after the fallback comment creation succeeds.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[732-733]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Draft unsupported spams errors 🐞 Bug ◔ Observability ⭐ New
Description
When gitlab.publish_code_suggestions_as_review=true but draft notes are unsupported/forbidden,
send_inline_comment() falls back to publishing live comments, yet publish_code_suggestions() still
attempts mr.draft_notes.list()/bulk_publish and can emit a misleading bulk-publish exception even
though suggestions were posted live.
Code

pr_agent/git_providers/gitlab_provider.py[R646-655]

+            if not created and as_draft:
+                # Draft notes are unavailable/erroring outright for this MR (unsupported GitLab
+                # version, insufficient permissions, ...) - degrade to a normal live comment rather
+                # than silently dropping the suggestion. It publishes immediately and falls outside
+                # the batch, which is an acceptable trade-off against losing it entirely.
+                get_logger().warning(
+                    f"Draft note creation failed for MR {self.id_mr}; retrying this suggestion as a "
+                    f"live comment instead of a draft")
+                created = self._create_suggestion_note(False, body, pos_obj, diff, target_file, relevant_file,
+                                                        original_suggestion, store, body_fp, code_fp)
Evidence
The PR adds a per-suggestion fallback from draft to live comments, but the final bulk-publish block
still executes whenever the feature flag is enabled, regardless of whether draft notes were
successfully created/usable, which can produce misleading exception logs and unnecessary API calls.

pr_agent/git_providers/gitlab_provider.py[601-656]
pr_agent/git_providers/gitlab_provider.py[756-820]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When draft-note creation fails and the code degrades to live comments, `publish_code_suggestions()` still runs the `as_review` bulk-publish block and may raise/log exceptions from `mr.draft_notes.list()` / `bulk_publish()`. This creates noisy/misleading error logs and extra API calls even though the suggestions were successfully delivered.

### Issue Context
- `send_inline_comment()` already has a per-suggestion fallback from draft to live.
- The end-of-method bulk-publish should only run when draft notes are actually usable (or at least when we have reason to believe pending drafts exist and are publishable).

### Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[644-656]
- pr_agent/git_providers/gitlab_provider.py[800-820]

### Suggested fix
- Track a local flag in `publish_code_suggestions()` (e.g., `draft_mode_worked` or `drafts_supported`) that is set to `True` only if at least one draft note was successfully created.
- If draft creation fails due to draft-notes unavailability and you fall back to live comments, set `drafts_supported=False` and:
 - stop attempting draft creation for subsequent suggestions in the same run, and
 - skip the final `draft_notes.list()/bulk_publish()` block.
- Also adjust the exception message in the bulk-publish `except` path to avoid asserting that drafts remain pending unless you know draft creation succeeded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Draft scan breaks dedup 🐞 Bug ☼ Reliability ⭐ New
Description
iter_existing_inline_comment_bodies() now unconditionally lists mr.draft_notes; if that call raises
(missing endpoint/permissions/transient error), InlineCommentStore.load() aborts the scan, so
pending-draft markers may be missed and the same suggestions can be re-queued as duplicates.
Code

pr_agent/algo/inline_comment_dedup.py[R130-136]

+        # gitlab.publish_code_suggestions_as_review queues suggestions as draft notes
+        # (invisible in the discussions/notes listings above until published). Scan
+        # them too, so a marker from a draft that's still pending - e.g. because a
+        # prior run's bulk-publish failed - is still seen, instead of being re-posted
+        # as a duplicate once it (or a fresh copy) is eventually published.
+        for draft in git_provider.mr.draft_notes.list(get_all=True):
+            yield getattr(draft, "note", "") or ""
Evidence
The PR adds an unconditional draft-notes listing to the GitLab dedup scanner. The store loader wraps
the entire scan in one try/except, so an exception thrown by the new draft listing prevents loading
markers from pending drafts (the motivation for adding this scan) and can lead to duplicate
re-posts.

pr_agent/algo/inline_comment_dedup.py[108-136]
pr_agent/algo/inline_comment_dedup.py[157-172]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`iter_existing_inline_comment_bodies()` for GitLab now calls `git_provider.mr.draft_notes.list(get_all=True)` without guarding for missing draft-note support or request failures. If that call throws, `InlineCommentStore.load()` catches the exception at a broad scope and stops scanning, which can prevent pending-draft markers from being loaded and allow duplicates to be posted.

### Issue Context
The draft-note scan was added specifically so markers in *pending* drafts (invisible via discussions/notes) are considered for dedup. A failure should degrade gracefully by skipping the draft scan, not by aborting the whole load.

### Fix Focus Areas
- pr_agent/algo/inline_comment_dedup.py[119-136]
- pr_agent/algo/inline_comment_dedup.py[157-172]

### Suggested fix
- Wrap the draft-notes listing in its own `try/except` inside the GitLab branch and continue if it fails.
- Optionally, check `hasattr(git_provider.mr, "draft_notes")` before attempting to list.
- Consider logging at debug/info specifically for the draft-note scan failure (without causing the broader “could not load existing comments” message to trigger).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
7. Pending drafts never published ✓ Resolved 🐞 Bug ☼ Reliability
Description
When gitlab.publish_code_suggestions_as_review is enabled, publish_code_suggestions() only calls
draft_notes.bulk_publish() if this run created at least one draft, but persistent inline-comment
dedup can cause all suggestions to be skipped due to markers found in existing pending drafts. After
a prior bulk_publish() failure, this can leave PR-Agent’s draft suggestions stuck invisible
indefinitely because no later run triggers bulk_publish().
Code

pr_agent/git_providers/gitlab_provider.py[R786-788]

+        if as_review and any_draft_created:
+            try:
+                self.mr.draft_notes.bulk_publish()
Evidence
publish_code_suggestions() only calls bulk_publish() when any_draft_created is true, but
send_inline_comment() returns False when persistent dedup detects a duplicate, and the dedup
scanner now considers pending draft notes. This combination means a run that skips all suggestions
due to existing pending draft markers will not publish them.

pr_agent/git_providers/gitlab_provider.py[737-799]
pr_agent/git_providers/gitlab_provider.py[612-625]
pr_agent/algo/inline_comment_dedup.py[108-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In GitLab batch-review mode (`gitlab.publish_code_suggestions_as_review=true`), `publish_code_suggestions()` gates `mr.draft_notes.bulk_publish()` behind `any_draft_created`. With `config.persistent_inline_comments=true`, `send_inline_comment()` can skip creating drafts when it detects a duplicate via markers found in *existing pending draft notes*. If a previous run created drafts but `bulk_publish()` failed, the next run may skip re-creating those drafts and therefore never call `bulk_publish()`, leaving drafts pending forever.
### Issue Context
- Draft notes are now included in the dedup scan.
- `send_inline_comment()` now returns `False` on duplicates and `publish_code_suggestions()` uses that to decide whether to call `bulk_publish()`.
### Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[737-799]
- pr_agent/git_providers/gitlab_provider.py[612-625]
- pr_agent/algo/inline_comment_dedup.py[108-136]
### Suggested fix approach
Adjust the bulk-publish gating so that batch mode can still publish previously-created PR-Agent drafts even when no *new* drafts were created in this run. For example:
- If `as_review` is enabled and `any_draft_created` is false, check whether there are pending draft notes that appear to belong to PR-Agent (e.g., contain the dedup marker or another explicit PR-Agent marker/prefix), and only then call `bulk_publish()`.
- Ensure this still avoids publishing unrelated manual drafts where possible (e.g., publish only if *all* pending drafts match the PR-Agent marker, or introduce a unique marker for PR-Agent drafts and verify presence before publishing).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Batch publishing default is off 📎 Requirement gap ⚙ Maintainability
Description
The new GitLab batching behavior is opt-in (publish_code_suggestions_as_review = false by
default), so /improve can still post multiple separate discussions and notifications by default.
This does not meet the requirement to publish multiple suggestions as a single consolidated
review/request to reduce notification noise.
Code

pr_agent/settings/configuration.toml[R296-299]

+# When pr_code_suggestions.commitable_code_suggestions is true, queue each suggestion as a GitLab draft
+# note and publish them all together in one batch (like GitLab's own "start a review" flow) instead of
+# posting each as its own live discussion - and its own notification - as soon as it's created.
+publish_code_suggestions_as_review = false
Evidence
PR Compliance ID 6 requires publishing multiple inline suggestions in a single consolidated event to
reduce notification noise, but the new setting defaults to false and the provider code also
defaults to False when reading it, leaving the default behavior unchanged (per-suggestion live
discussions).

Publish /improve inline code suggestions in a single review request to reduce notification noise
pr_agent/settings/configuration.toml[296-299]
pr_agent/git_providers/gitlab_provider.py[733-737]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The batching feature is implemented but disabled by default, so the default behavior still creates one live discussion per suggestion on GitLab.
## Issue Context
Compliance requires that multiple `/improve` inline suggestions be published in a single consolidated review/request (or equivalent single notification event) rather than multiple separate notifications.
## Fix Focus Areas
- pr_agent/settings/configuration.toml[296-299]
- pr_agent/git_providers/gitlab_provider.py[733-737]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Dedup misses GitLab drafts ✓ Resolved 🐞 Bug ☼ Reliability
Description
In draft-review mode, send_inline_comment() writes markers into draft notes, but
InlineCommentStore’s GitLab scanner only reads discussions and regular notes. If drafts remain
pending (e.g., bulk_publish failure), later runs won’t see those markers and can create duplicate
suggestions that will all publish later.
Code

pr_agent/git_providers/gitlab_provider.py[R643-645]

+                if as_draft:
+                    self.mr.draft_notes.create({'note': body, 'position': pos_obj})
+                else:
Evidence
The PR introduces draft-note publishing via mr.draft_notes.create, while the dedup store only scans
published discussions and notes; therefore pending drafts are invisible to dedup and can be
duplicated on later runs if not published.

pr_agent/git_providers/gitlab_provider.py[601-707]
pr_agent/algo/inline_comment_dedup.py[108-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
With `as_draft=True`, inline suggestions are created via `mr.draft_notes.create(...)`. Persistent inline-comment dedup relies on scanning existing comments for fingerprint markers, but the GitLab implementation only scans `mr.discussions.list()` and `mr.notes.list()`; it never scans `mr.draft_notes`. If draft notes are left pending (bulk-publish failure or process exit before publishing), a subsequent run will not detect them and will re-create duplicates.
### Issue Context
Draft notes are visible only to the posting user until published, so they are not present in the normal discussions/notes streams that dedup scans today.
### Fix Focus Areas
- pr_agent/algo/inline_comment_dedup.py[108-129]
- pr_agent/git_providers/gitlab_provider.py[601-708]
### Suggested fix
- Extend `iter_existing_inline_comment_bodies()` for `GitLabProvider` to also list draft notes (best-effort), e.g.:
- `for dn in git_provider.mr.draft_notes.list(get_all=True): yield getattr(dn, "note", "") or getattr(dn, "body", "") or ""`
- Keep it wrapped in the existing try/except behavior so providers/environments without draft-notes support degrade safely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context

Grey Divider

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.
Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread pr_agent/git_providers/gitlab_provider.py
Comment thread pr_agent/git_providers/gitlab_provider.py Outdated
Addresses two valid findings from the Qodo automated review on this PR:

- publish_code_suggestions() unconditionally called
  mr.draft_notes.bulk_publish() whenever publish_code_suggestions_as_review
  was enabled, even if this invocation queued zero drafts (e.g. an empty
  or all-failed-to-parse suggestion list). That could publish unrelated
  drafts already pending on the MR from a previous run. send_inline_comment
  now reports whether it actually created something, and bulk_publish is
  only called when at least one draft was queued this call.

- The persistent-inline-comments dedup scanner for GitLab only scanned
  discussions and plain notes, missing draft notes queued by the new
  batching mode. A suggestion whose draft is still pending (e.g. after a
  bulk-publish failure) would not be recognized on a later run and could
  be re-queued as a duplicate. iter_existing_inline_comment_bodies now
  also scans mr.draft_notes.list().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nlinakis-xm

Copy link
Copy Markdown
Contributor Author

Thanks for the review - going through all 4 findings:

Fixed (2, 4):

  • Add support for OpenAI organization in the secrets file #2 "Bulk-publishes stale drafts" - valid bug. bulk_publish() was called unconditionally whenever as_review was on, even with zero drafts queued this call (empty list, or every suggestion failing to parse), which could publish unrelated drafts already pending on the MR from an earlier run. send_inline_comment() now returns whether it actually created something, and bulk_publish() only fires if at least one draft was queued in this invocation. Added regression tests for both the empty-list and all-fail cases.
  • Merge CLI scripts #4 "Dedup misses GitLab drafts" - valid. iter_existing_inline_comment_bodies() scanned discussions and notes but not mr.draft_notes.list(), so a suggestion whose draft was still pending (e.g. after a bulk_publish failure) wouldn't be recognized by persistent_inline_comments on a later run and could be re-queued as a duplicate. Now scans draft notes too, following the same pattern already used for the notes fallback. Added a test.

Not applying as suggested (1, 3), with reasoning:

  • delete "Preparing review..." comment #1 "bulk_publish failure returns True" - publish_code_suggestions() has always unconditionally returned True for this provider, even when individual suggestions fail (see the pre-existing comment right above the return True: "we publish suggestions one-by-one, so if one fails, the rest will still be published"). Flipping this to False on bulk_publish failure would route into pr_code_suggestions.py's generic per-suggestion retry fallback - but in as_review mode that fallback would recreate a fresh draft note for every suggestion (since the ones from this run already exist as pending drafts, just unpublished) and re-attempt bulk_publish() per suggestion, producing duplicate drafts rather than fixing anything. The failure is already surfaced via get_logger().exception(...), consistent with how this method already handles per-suggestion creation failures. I don't think returning False here is safe without also reworking that shared retry path, which is out of scope for this change.

  • Combine all modified and deleted files that been compressed to the prompt #3 "Batch publishing default is off" - this is intentional, not a gap. Unlike GitHub's create_review(), which has no visibility side effect, GitLab draft notes are invisible until published - if bulk_publish() ever fails (permissions, an older self-hosted GitLab without draft-notes support, a network blip), suggestions that used to reliably post live would silently vanish into invisible drafts for every existing deployment on upgrade. Every comparable behavior-changing toggle already in this codebase (persistent_inline_comments, restricted_mode, output_run_details, etc.) ships opt-in for the same reason. Happy to revisit if there's a stronger case for defaulting it on, but I'd want to see it land as opt-in first.

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Comment on lines +645 to +648
if as_draft:
self.mr.draft_notes.create({'note': body, 'position': pos_obj})
else:
self.mr.discussions.create({'body': body, 'position': pos_obj})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Draft failure drops suggestions 🐞 Bug ☼ Reliability

When as_draft=True, send_inline_comment() uses mr.draft_notes.create() for both the primary
inline comment and the fallback general note, and never falls back to the previous live
discussions.create()/notes.create() behavior if draft-note creation fails. If the draft-notes
endpoint is unsupported/unavailable or consistently errors (e.g., 403/404), batch mode can fail to
post suggestions at all.
Agent Prompt
### Issue description
In batch mode, `send_inline_comment(..., as_draft=True)` posts via `mr.draft_notes.create()` in both the primary (inline) and fallback (general note) paths. If draft-note creation fails due to environment/API constraints, the code retries another draft-note creation in the fallback path and ultimately returns `False` without attempting to post a live discussion/note. This can cause suggestions to be silently dropped for users who enable batch mode but cannot use draft notes.

### Issue Context
The pre-PR behavior (`as_draft=False`) used `mr.discussions.create()` for inline and `mr.notes.create()` for fallback. Batch mode should degrade gracefully back to that behavior when draft notes are not usable.

### Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[643-717]

### Suggested fix approach
- When `as_draft=True` and `mr.draft_notes.create(...)` raises, detect “drafts unavailable” style failures (or conservatively any exception) and retry using the live endpoints:
  - Primary retry: `mr.discussions.create({'body': body, 'position': pos_obj})`
  - Fallback retry: `mr.notes.create({'body': body_fallback, 'position': fallback_position})`
- Ensure `publish_code_suggestions()` does not call `bulk_publish()` if the run fell back to live comments (or track draft-vs-live separately).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit f2a4e00

…lk-publish on real MR state

Addresses two more findings from the Qodo review round on this PR:

- "Draft failure drops suggestions": when as_draft=True, both the primary
  and fallback comment-creation attempts used mr.draft_notes.create(). If
  draft notes are unsupported/erroring outright for an MR (older
  self-hosted GitLab, permission gap), every suggestion in a batch run was
  silently dropped instead of posting live as they would with the flag
  off. The create+fallback logic is now extracted into
  _create_suggestion_note(); if a suggestion's draft attempt fails
  completely, send_inline_comment retries the same suggestion live
  instead of giving up on it.

- "Pending drafts never published": the previous fix gated bulk_publish()
  on whether *this* call created a draft, but persistent_inline_comments
  dedup can now also match markers embedded in still-pending drafts
  (e.g. left over from an earlier bulk_publish failure). A run where every
  suggestion is skipped as a duplicate of one of those pending drafts
  would never call bulk_publish(), leaving them stuck forever.
  publish_code_suggestions() now checks the MR's actual pending drafts
  (mr.draft_notes.list()) instead of in-memory tracking, so it keeps
  retrying a stuck publish even when this run queued nothing new.

Extended tests for both scenarios, plus a stateful draft_notes test
fixture (create/list/bulk_publish behave like the real API) instead of
asserting on call counts alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nlinakis-xm

Copy link
Copy Markdown
Contributor Author

Second round - the 2 new findings, both valid, fixed:

Fixed:

  • "Draft failure drops suggestions" - valid. Both the primary and fallback tiers used mr.draft_notes.create() when as_draft=True, with no way back to live posting if drafts are unsupported/erroring outright for an MR. Extracted the create+fallback logic into _create_suggestion_note(); send_inline_comment() now retries the same suggestion live if the draft attempt fails completely, instead of dropping it. Added a test forcing draft_notes.create to always raise and asserting the suggestion still lands via discussions.create.
  • "Pending drafts never published" - valid, and a real regression from my own previous fix in this same PR. Gating bulk_publish() on an in-memory "did this call create a draft" flag breaks once dedup can also match markers on pending drafts (which it now can, from the earlier fix): a run where every suggestion is skipped as a duplicate of a still-stuck draft would never retry publishing it. Replaced the in-memory flag with a direct check of mr.draft_notes.list() - the MR's actual state - so a stuck publish keeps getting retried even when a run queues nothing new. This also subsumes the original "stale drafts" fix (checking real state is strictly more correct than tracking this-call-only creations). Added a regression test that seeds a "stuck" pending draft matching a dedup marker and confirms bulk_publish() still fires even though the run creates zero new drafts.

Also switched the test fixture's draft_notes mock from static return values to a small stateful fake (create → queues, list → reflects the queue, bulk_publish → clears it), since asserting call counts alone wasn't going to catch either of the bugs above.

Carried over from the previous round, still declined for the same reasons already given (not re-litigating in full here, see the earlier reply above):

  • "bulk_publish failure returns True" - flipping this would route into the shared per-suggestion retry fallback, which isn't safe for draft mode (would recreate fresh drafts for suggestions that already exist as pending, then attempt bulk_publish per suggestion).

  • "Batch publishing default is off" - intentional; matches every other behavior-changing toggle already in this codebase, and avoids a silent-invisible-drafts failure mode for every existing deployment on upgrade.

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 533f2b7

score = original_suggestion.get('score', 7)

if hasattr(self, 'main_language'):
language = self.main_language
if hasattr(self, 'main_language'):
language = self.main_language
else:
language = ''
# Conflicts:
#	pr_agent/algo/inline_comment_dedup.py
…ebug block

Where draft notes are unavailable, every suggestion has already degraded to a
live discussion, so a failing draft_notes.list() must not log them as pending
drafts needing a manual publish from the UI.
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (6) 📜 Skill insights (0)

Grey Divider


Action required

1. Publishes unrelated pending drafts 🐞 Bug ≡ Correctness
Description
bulk_publish() acts on the MR's entire pending draft collection, so when this run queues a
suggestion it also publishes any unrelated manual or earlier drafts owned by the same GitLab user.
The pending check only avoids this when the collection is empty; it does not isolate the drafts
created by this batch.
Code

pr_agent/git_providers/gitlab_provider.py[R1158-1159]

+                if pending:
+                    self.mr.draft_notes.bulk_publish()
Relevance

●●● Strong

The unfiltered bulk operation can publish unrelated drafts, contradicting the feature’s stated
isolation goal.

PR-#2797

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code lists the complete MR draft-note collection and invokes an unfiltered bulk operation. The
new test itself acknowledges that this operation can publish unrelated drafts, while its fake models
bulk publication by clearing the whole pending queue.

pr_agent/git_providers/gitlab_provider.py[1142-1159]
tests/unittest/test_gitlab_batch_publish_suggestions.py[49-65]
tests/unittest/test_gitlab_batch_publish_suggestions.py[170-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Batch publication currently publishes every pending draft on the MR, including unrelated drafts from the same GitLab user.

## Issue Context
GitLab's MR-level `bulk_publish()` is not scoped to the notes created by this invocation. Track and distinguish PR-Agent-owned drafts, and do not invoke the MR-wide operation when unrelated drafts are pending; use a safe fallback when isolation cannot be guaranteed.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1142-1159]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[170-181]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. List failure strands drafts 🐞 Bug ☼ Reliability
Description
If draft creation succeeds but the subsequent draft_notes.list() call fails, the handler sets
pending to an empty list and skips bulk_publish(). Those queued suggestions remain invisible to
reviewers even though publish_code_suggestions() returns success; the comment claiming every
suggestion already degraded to a live comment is only true when creation, not listing, failed.
Code

pr_agent/git_providers/gitlab_provider.py[R1153-1157]

+                except Exception as e:
+                    # Draft notes are unusable on this instance/token; send_inline_comment has
+                    # already degraded every suggestion to a live comment, so nothing is pending.
+                    get_logger().warning(f"Could not list draft notes for MR {self.id_mr}: {e}")
+                    pending = []
Relevance

●●● Strong

This is a concrete failure-path data-loss issue; recent provider reviews consistently accepted
defensive reliability fixes.

PR-#2806
PR-#2796

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Draft creation and the later listing request are independent: successful creation returns True,
but a later listing exception forces pending=[], bypasses bulk publication, and the method
ultimately returns True.

pr_agent/git_providers/gitlab_provider.py[1007-1015]
pr_agent/git_providers/gitlab_provider.py[1136-1159]
pr_agent/git_providers/gitlab_provider.py[1171-1172]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A transient failure while listing draft notes suppresses publication of drafts that were already created successfully.

## Issue Context
Track whether this invocation successfully queued drafts independently of the listing request. On listing failure, either safely attempt publication/retry it or return a failure that accurately signals the unpublished batch; do not replace an unknown state with an empty collection.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1136-1159]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[97-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Publish failure reports success 🐞 Bug ☼ Reliability
Description
When bulk_publish() fails, the exception is only logged and publish_code_suggestions() still
returns True, so callers do not run their existing retry path while every queued suggestion
remains visible only to the posting user. A transient final API failure therefore silently
suppresses the whole review until an unrelated future run or manual action occurs.
Code

pr_agent/git_providers/gitlab_provider.py[R1166-1169]

+                get_logger().exception(
+                    f"Failed to bulk-publish draft code-suggestion notes for MR {self.id_mr}; they remain "
+                    f"as pending drafts, visible only to the posting user, until published manually from "
+                    f"the GitLab UI or by a subsequent successful run: {e}")
Relevance

●● Moderate

The failure semantics are risky, but the PR explicitly tests and documents non-propagation, leaving
team intent mixed.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The catch block states that failed publication leaves drafts invisible, but execution then reaches
the unconditional True return. The main caller retries only when this result is false.

pr_agent/git_providers/gitlab_provider.py[1160-1172]
pr_agent/tools/pr_code_suggestions.py[685-689]
tests/unittest/test_gitlab_batch_publish_suggestions.py[157-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed bulk publication is reported as successful to callers even though no reviewer can see the queued suggestions.

## Issue Context
Retry the bulk-publication operation safely and/or return an accurate failure result after exhausting retries. Ensure caller retries cannot enqueue duplicate drafts, especially when persistent inline deduplication is disabled.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1160-1172]
- pr_agent/tools/pr_code_suggestions.py[685-689]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[157-167]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. language is never used 📘 Rule violation ✧ Quality
Description
_create_suggestion_note() assigns language on both branches but never reads it, triggering the
enabled Pyflakes F841 check. This prevents the changed Python file from passing the required
zero-warning lint check.
Code

pr_agent/git_providers/gitlab_provider.py[R1039-1042]

+                if hasattr(self, 'main_language'):
+                    language = self.main_language
+                else:
+                    language = ''
Relevance

●●● Strong

Unused newly assigned local is a deterministic lint issue, and historical dead-code cleanup was
accepted.

PR-#2381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2694666 requires changed Python files to produce no lint errors or warnings. The
cited branch assigns language in both paths, while the subsequent fallback construction never
consumes it.

Rule 2694666: Python code must pass flake8 in CI with zero errors or warnings
pr_agent/git_providers/gitlab_provider.py[1039-1042]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_create_suggestion_note()` assigns the local variable `language` but never uses it, causing an `F841` lint violation.

## Issue Context
The project enables Pyflakes `F` checks, and changed Python code must pass with zero errors or warnings.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1039-1042]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Provider adds single-quoted literals 📘 Rule violation ⚙ Maintainability
Description
The new suggestion-note helper uses single-quoted Python string literals for payload keys and
values. This violates the required double-quote convention for changed Python strings.
Code

pr_agent/git_providers/gitlab_provider.py[R1008-1011]

+            if as_draft:
+                self.mr.draft_notes.create({'note': body, 'position': pos_obj})
+            else:
                self.mr.discussions.create({'body': body, 'position': pos_obj})
Relevance

●●● Strong

Recent same-day precedent accepted converting newly added single-quoted Python literals to double
quotes.

PR-#2796

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694657 requires all non-docstring Python literals to use double quotes. The added calls
visibly use literals such as 'note', 'position', and 'body'.

Rule 2694657: Use double quotes for all Python string literals
pr_agent/git_providers/gitlab_provider.py[1008-1011]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New Python string literals in the GitLab suggestion-note implementation use single quotes instead of the mandated double quotes.

## Issue Context
Apply the conversion consistently throughout all lines added by this PR in the provider helper, except where double quotes would require additional escaping.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1008-1069]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Fallback line exceeds 120 📘 Rule violation ⚙ Maintainability
Description
The added body_fallback concatenation exceeds the configured 120-character maximum. It violates
both the Python-specific Ruff requirement and the general source line-length rule.
Code

pr_agent/git_providers/gitlab_provider.py[1045]

+                body_fallback +=f"\n\n<details><summary>[{target_file.filename} [{line_start}-{line_end}]]({link}):</summary>\n\n"
Relevance

●●● Strong

Recent precedents accepted wrapping overlong modified Python lines to satisfy the 120-character
limit.

PR-#2776
PR-#2424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
pyproject.toml configures Ruff with line-length = 120, and the cited added Python line is longer
than that limit. Rules 2694655 and 2694690 both prohibit such lines.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
Rule 2694690: Enforce maximum line length of 120 characters
pr_agent/git_providers/gitlab_provider.py[1045-1045]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The added fallback-body expression exceeds 120 characters on one physical Python line.

## Issue Context
Wrap the expression with parentheses and split the formatted string without changing its rendered content.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1044-1047]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
7. Documentation lines exceed 120 📘 Rule violation ⚙ Maintainability
Description
The new GitLab configuration documentation is written as very long physical lines well beyond 120
characters. This violates the general maximum line-length requirement for modified non-generated
source files.
Code

docs/docs/tools/improve.md[231]

+By default, when `commitable_code_suggestions` is enabled, GitLab posts each suggestion as its own live discussion as soon as it's created - which means a separate notification (and email, if configured) per suggestion. To instead queue all suggestions and publish them together in a single batch, similar to using "start a review" in the GitLab UI, enable:
Relevance

●●● Strong

Recent same-day precedent accepted wrapping long documentation lines in modified docs.

PR-#2797

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694690 requires every modified non-generated source file to keep physical lines at or below
120 characters. The cited added paragraph substantially exceeds that maximum; line 238 does as well.

Rule 2694690: Enforce maximum line length of 120 characters
docs/docs/tools/improve.md[231-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added Markdown section contains paragraphs longer than the 120-character source-line limit.

## Issue Context
Reflow the prose while preserving the rendered Markdown and configuration example.

## Fix Focus Areas
- docs/docs/tools/improve.md[227-238]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Helper docstring is descriptive 📘 Rule violation ⚙ Maintainability
Description
The new _create_suggestion_note() docstring begins with Creates and later uses Returns, rather
than imperative Create and Return. This violates the imperative-phrasing requirement for changed
docstrings.
Code

pr_agent/git_providers/gitlab_provider.py[R1004-1006]

+        """Creates the anchored suggestion comment, falling back to a general file note if GitLab
+        rejects the position (e.g. the suggestion isn't on a '+' line). Returns True iff either
+        attempt succeeded."""
Relevance

●●● Strong

Recent provider precedent accepted rewriting descriptive comments/docstrings into imperative
phrasing.

PR-#2797

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694688 explicitly rejects third-person docstring openings such as Returns. The cited new
docstring starts with Creates and contains Returns, both descriptive rather than imperative.

Rule 2694688: Docstrings and comments must use imperative phrasing
pr_agent/git_providers/gitlab_provider.py[1004-1006]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new helper docstring uses descriptive third-person verbs instead of imperative phrasing.

## Issue Context
Rewrite `Creates` as `Create` and `Returns` as `Return` while retaining the behavioral explanation. Review the other newly added docstring for the same issue.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[949-949]
- pr_agent/git_providers/gitlab_provider.py[1004-1006]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Tests add single-quoted literals 📘 Rule violation ⚙ Maintainability
Description
The new unit test module repeatedly introduces single-quoted Python literals in suggestion fixtures
and assertions. These literals do not qualify for the escaping exception and violate the required
double-quote convention.
Code

tests/unittest/test_gitlab_batch_publish_suggestions.py[R21-24]

+        'body': "**Suggestion:** fix it\n```suggestion\nx = 2\n```",
+        'relevant_file': 'a.py',
+        'relevant_lines_start': 2,
+        'relevant_lines_end': 2,
Relevance

●● Moderate

Quote-style evidence is mixed: recent provider literals were corrected, while a comparable
test-literal finding was rejected.

PR-#2796
PR-#2598

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694657 applies to changed Python source, including tests. The cited new fixture contains
ordinary single-quoted keys and values such as 'body', 'relevant_file', and 'a.py'.

Rule 2694657: Use double quotes for all Python string literals
tests/unittest/test_gitlab_batch_publish_suggestions.py[21-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added test module uses single quotes for ordinary Python string literals.

## Issue Context
Convert all affected literals in the new test file, including fixture dictionary keys, values, assertion keys, and suggestion-fence replacements.

## Fix Focus Areas
- tests/unittest/test_gitlab_batch_publish_suggestions.py[19-222]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: ⚖️ Balanced: This changes GitLab comment delivery, draft-note fallback behavior, deduplication, and batch publishing across provider and configuration paths, creating real notification and visibility risks, but it is not sufficiently bug-dense across independent paths to justify extended review.
ⓘ  3 issues published inline · 9 in summary

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1158 to +1159
if pending:
self.mr.draft_notes.bulk_publish()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

7. Publishes unrelated pending drafts 🐞 Bug ≡ Correctness

bulk_publish() acts on the MR's entire pending draft collection, so when this run queues a
suggestion it also publishes any unrelated manual or earlier drafts owned by the same GitLab user.
The pending check only avoids this when the collection is empty; it does not isolate the drafts
created by this batch.
Agent Prompt
## Issue description
Batch publication currently publishes every pending draft on the MR, including unrelated drafts from the same GitLab user.

## Issue Context
GitLab's MR-level `bulk_publish()` is not scoped to the notes created by this invocation. Track and distinguish PR-Agent-owned drafts, and do not invoke the MR-wide operation when unrelated drafts are pending; use a safe fallback when isolation cannot be guaranteed.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1142-1159]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[170-181]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1153 to +1157
except Exception as e:
# Draft notes are unusable on this instance/token; send_inline_comment has
# already degraded every suggestion to a live comment, so nothing is pending.
get_logger().warning(f"Could not list draft notes for MR {self.id_mr}: {e}")
pending = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

8. List failure strands drafts 🐞 Bug ☼ Reliability

If draft creation succeeds but the subsequent draft_notes.list() call fails, the handler sets
pending to an empty list and skips bulk_publish(). Those queued suggestions remain invisible to
reviewers even though publish_code_suggestions() returns success; the comment claiming every
suggestion already degraded to a live comment is only true when creation, not listing, failed.
Agent Prompt
## Issue description
A transient failure while listing draft notes suppresses publication of drafts that were already created successfully.

## Issue Context
Track whether this invocation successfully queued drafts independently of the listing request. On listing failure, either safely attempt publication/retry it or return a failure that accurately signals the unpublished batch; do not replace an unknown state with an empty collection.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1136-1159]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[97-111]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1166 to +1169
get_logger().exception(
f"Failed to bulk-publish draft code-suggestion notes for MR {self.id_mr}; they remain "
f"as pending drafts, visible only to the posting user, until published manually from "
f"the GitLab UI or by a subsequent successful run: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

9. Publish failure reports success 🐞 Bug ☼ Reliability

When bulk_publish() fails, the exception is only logged and publish_code_suggestions() still
returns True, so callers do not run their existing retry path while every queued suggestion
remains visible only to the posting user. A transient final API failure therefore silently
suppresses the whole review until an unrelated future run or manual action occurs.
Agent Prompt
## Issue description
A failed bulk publication is reported as successful to callers even though no reviewer can see the queued suggestions.

## Issue Context
Retry the bulk-publication operation safely and/or return an accurate failure result after exhausting retries. Ensure caller retries cannot enqueue duplicate drafts, especially when persistent inline deduplication is disabled.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[1160-1172]
- pr_agent/tools/pr_code_suggestions.py[685-689]
- tests/unittest/test_gitlab_batch_publish_suggestions.py[157-167]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. I pushed the rebase and both fixes myself rather than send you round again, each on its own commit, and refreshed the description.

The batching holds up under test, and the third-round rework is the right shape: reading the MR's real pending-draft list rather than an in-memory flag means a stuck publish gets retried on the next run instead of stranding drafts. I am content with both findings you declined. Opt-in is right when drafts stay invisible until published, and the return-True argument stands now that the retry exists.

The one thing worth knowing: where draft notes are unavailable, every suggestion correctly degraded to a live comment and then the pending-draft listing failed too, so the handler reported them as stuck drafts needing a manual publish. They were already live. That is the fix in the second commit. The conflict itself was a single keep-both hunk from #2651, not #2774.

Thanks for turning three rounds around in a day, and sorry it then sat a fortnight.

p.mr.draft_notes.bulk_publish.assert_called_once()


def test_empty_suggestions_does_not_bulk_publish_unrelated_pending_drafts():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One note for later, no change needed: this does not quite prove what the comment above it claims. The fixture starts with no pending drafts, so it only shows that nothing-pending means no publish. Seed one unrelated pending draft and an empty run does publish it, because the gate is the MR's real state. That is the trade you chose in round two and I think it is the right one, but the name reads as a guarantee it is not making.

@IsmaelMartinez
IsmaelMartinez merged commit 38cc56b into The-PR-Agent:main Aug 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants