Skip to content

fix: don't overwrite PR title in /describe when generate_ai_title is false#2475

Merged
naorpeled merged 4 commits into
The-PR-Agent:mainfrom
IsmaelMartinez:fix/describe-preserve-title-when-not-ai-generated
Jun 26, 2026
Merged

fix: don't overwrite PR title in /describe when generate_ai_title is false#2475
naorpeled merged 4 commits into
The-PR-Agent:mainfrom
IsmaelMartinez:fix/describe-preserve-title-when-not-ai-generated

Conversation

@IsmaelMartinez

Copy link
Copy Markdown
Contributor

What

/describe re-saved the PR/MR title on every run even when generate_ai_title is false (the default). The title is read at the start of the describe run and written back unconditionally, so a title edited by hand while describe is running gets reverted on publish. This affects every provider, since each one sets the title alongside the description in publish_description.

Fix

When generate_ai_title is false, the describe flow now passes pr_title=None, and each provider treats None as "leave the existing title unchanged", updating only the description. When generate_ai_title is true, behaviour is unchanged. Bitbucket Server is special-cased: its update replaces the PR, so it preserves the existing title instead of blanking it.

Tests

Added GitLab provider unit tests covering both the title-preserved (None) and title-updated paths.

Closes #2474

…false

publish_description re-saved the PR/MR title on every describe run even when
the title was not AI-generated. Because the title is read at job start and
written back unconditionally, a manual title edit made while describe is
running gets reverted. This affected every provider, since they all set the
title alongside the description.

Pass None as the title from the describe flow when generate_ai_title is false,
and update each provider to leave the existing title unchanged in that case
(updating only the description).

Refs: The-PR-Agent#2474
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the bug label Jun 26, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Preserve manually edited PR title when /describe runs without AI title
🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Description

• Stop /describe from re-writing PR titles when generate_ai_title is disabled.
• Treat pr_title=None as “update description only” across all git providers.
• Add GitLab unit tests covering title-preserved and title-updated publish paths.
Diagram

graph TD
A["/describe run"] --> B{"AI title enabled?"}
B -->|"yes"| C["publish_description(title, body)"] --> D["GitProvider impls"] --> E{{"SCM APIs"}}
B -->|"no"| F["publish_description(None, body)"] --> D
D --> G["Bitbucket Server"] --> E

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

The following are alternative approaches to this PR:

1. Split API into publish_body() and publish_title_and_body()
  • ➕ Avoids overloading None semantics and clarifies intent at call sites
  • ➕ Makes provider implementations harder to misuse
  • ➖ Larger interface change across all providers and call sites
  • ➖ Potentially more churn/backwards-compat concerns if external integrations exist
2. Re-fetch title just before publishing
  • ➕ Preserves manual edits without changing provider method contracts
  • ➖ Extra API calls on every /describe publish
  • ➖ Still races if title changes between re-fetch and update; doesn’t help providers that require full-object updates
3. Pass explicit flag (update_title: bool) alongside title/body
  • ➕ Keeps title as a required string while making behavior explicit
  • ➖ Propagates additional parameter across all providers
  • ➖ More verbose than using Optional[str] for a single conditional behavior

Recommendation: Current approach (pass pr_title=None when generate_ai_title is false and have providers treat None as “do not update title”) is the smallest, most compatible fix and directly addresses the race with manual edits. Consider tightening type hints to Optional[str] in signatures in a follow-up for clarity, but the behavioral change is sound; the Bitbucket Server preservation logic is the right exception due to its replace-style updates.

Files changed (12) +63 / -16

Bug fix (10) +37 / -16
azuredevops_provider.pySkip Azure DevOps PR title update when pr_title is None +2/-1

Skip Azure DevOps PR title update when pr_title is None

• Guards setting updated_pr.title so title is only sent when pr_title is provided. This prevents reverting a manually edited title during /describe publishes.

pr_agent/git_providers/azuredevops_provider.py

bitbucket_provider.pyOmit Bitbucket Cloud title field when pr_title is None +4/-5

Omit Bitbucket Cloud title field when pr_title is None

• Builds the PUT payload as a dict and conditionally includes the title key. Allows description-only updates without overwriting the existing PR title.

pr_agent/git_providers/bitbucket_provider.py

bitbucket_server_provider.pyPreserve existing Bitbucket Server title on replace-style updates +4/-1

Preserve existing Bitbucket Server title on replace-style updates

• Ensures the payload always includes a title: uses existing PR title when pr_title is None. Avoids blanking the title because Bitbucket Server updates replace the PR object.

pr_agent/git_providers/bitbucket_server_provider.py

codecommit_client.pyAvoid CodeCommit title update when pr_title is None +3/-1

Avoid CodeCommit title update when pr_title is None

• Conditionally calls update_pull_request_title only when a title is provided. Always updates the description as before.

pr_agent/git_providers/codecommit_client.py

gerrit_provider.pyPublish Gerrit description without prepending title when pr_title is None +2/-1

Publish Gerrit description without prepending title when pr_title is None

• Builds the comment text as either just the message or title + message depending on pr_title. Prevents re-posting the original title when not AI-generated.

pr_agent/git_providers/gerrit_provider.py

gitea_provider.pyConditionally include title in Gitea edit_pull_request call +6/-3

Conditionally include title in Gitea edit_pull_request call

• Refactors edit_pull_request parameters into kwargs and only adds title when pr_title is not None. Enables body-only updates without title overwrite.

pr_agent/git_providers/gitea_provider.py

github_provider.pyUse GitHub API body-only edit when pr_title is None +5/-1

Use GitHub API body-only edit when pr_title is None

• Branches publish_description to call pr.edit(body=...) when pr_title is None, otherwise edits both title and body. Preserves existing titles during describe runs without AI titles.

pr_agent/git_providers/github_provider.py

gitlab_provider.pyOnly set GitLab MR title when pr_title is provided +2/-1

Only set GitLab MR title when pr_title is provided

• Guards assigning self.mr.title so None means “leave unchanged”. Continues to update description and save the MR.

pr_agent/git_providers/gitlab_provider.py

local_git_provider.pyWrite local description file without title line when pr_title is None +2/-1

Write local description file without title line when pr_title is None

• Adjusts output content to include only body when pr_title is None. Keeps prior behavior (title + body) when a title is provided.

pr_agent/git_providers/local_git_provider.py

pr_description.pyPass None title to providers when AI title generation is disabled +7/-1

Pass None title to providers when AI title generation is disabled

• Changes the publish path to compute title_to_publish based on generate_ai_title. Prevents a race where re-writing the original title reverts manual edits made during /describe execution (issue #2474).

pr_agent/tools/pr_description.py

Tests (1) +23 / -0
test_gitlab_provider.pyAdd GitLab tests for title-preserved vs title-updated publishing +23/-0

Add GitLab tests for title-preserved vs title-updated publishing

• Introduces unit tests verifying publish_description(None, ...) leaves the MR title unchanged, and publish_description("AI title", ...) updates both title and description.

tests/unittest/test_gitlab_provider.py

Documentation (1) +3 / -0
git_provider.pyDocument pr_title=None contract for publish_description +3/-0

Document pr_title=None contract for publish_description

• Adds inline documentation that pr_title may be None to mean “leave title unchanged” and providers must not write the title in that case.

pr_agent/git_providers/git_provider.py

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

qodo-free-for-open-source-projects Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale title fallback ✓ Resolved 🐞 Bug ≡ Correctness
Description
BitbucketServerProvider.publish_description() falls back to the cached self.pr.title when
pr_title is None, so a manual title edit made while /describe is running can still be reverted
on publish. This undermines the intended fix for Bitbucket Server because self.pr is only fetched
once in set_pr() and not refreshed before update.
Code

pr_agent/git_providers/bitbucket_server_provider.py[R516-523]

 payload = {
     "version": self.pr.version,
     "description": description,
-            "title": pr_title,
+            # The update replaces the PR, so omitted fields get wiped. When
+            # pr_title is None (title not AI-generated) keep the existing title
+            # rather than blanking it.
+            "title": pr_title if pr_title is not None else self.pr.title,
     "reviewers": self.pr.reviewers  # needs to be sent otherwise gets wiped
Evidence
/describe captures the PR title early and later publishes with pr_title=None when AI title
generation is disabled; Bitbucket Server then uses self.pr.title as the fallback title. Because
Bitbucket Server’s self.pr is only populated once in set_pr() and not refreshed before
publishing, self.pr.title can be stale and can overwrite a title edited while describe is running.

pr_agent/git_providers/bitbucket_server_provider.py[197-200]
pr_agent/git_providers/bitbucket_server_provider.py[514-524]
pr_agent/tools/pr_description.py[63-66]
pr_agent/tools/pr_description.py[183-189]

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

## Issue description
Bitbucket Server uses `self.pr.title` as a fallback when `pr_title is None`, but `self.pr` is fetched once at `set_pr()` time and can be stale by the time `/describe` publishes. If a user edits the PR title during the describe run, the publish call can still send the old title and revert the user’s change.
## Issue Context
- `/describe` now passes `pr_title=None` when `generate_ai_title` is false.
- Bitbucket Server cannot omit `title` in the update payload (omitted fields get wiped), so it must supply a title value.
- To actually “leave title unchanged”, the provider should fetch the latest PR state right before updating, and use that latest title/version/reviewers as the fallback.
## Fix Focus Areas
- pr_agent/git_providers/bitbucket_server_provider.py[197-200]
- pr_agent/git_providers/bitbucket_server_provider.py[514-529]
## Proposed fix
In `publish_description`, if `pr_title is None`, re-fetch the PR (`latest_pr = self._get_pr()`), and build the payload using `latest_pr.title`, `latest_pr.version`, and `latest_pr.reviewers` (and optionally update `self.pr = latest_pr`) so the update preserves the most recent server-side title rather than a cached snapshot.

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



Remediation recommended

2. Local output drops title ✓ Resolved 🐞 Bug ≡ Correctness
Description
LocalGitProvider.publish_description() writes only pr_body when pr_title is None, which removes the
title line from the persisted description.md output and violates the new "leave existing title
unchanged" contract. With generate_ai_title=false (default), PRDescription now passes None, so local
/describe publishes will lose the title content.
Code

pr_agent/git_providers/local_git_provider.py[R115-116]

+            content = pr_body if pr_title is None else pr_title + '\n' + pr_body
+            file.write(content)
Evidence
PRDescription passes None for pr_title when AI title generation is disabled, and
LocalGitProvider overwrites the output file with body-only in that case, causing the title to be
lost from the persisted output.

pr_agent/tools/pr_description.py[171-190]
pr_agent/git_providers/local_git_provider.py[112-116]

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

## Issue description
`LocalGitProvider.publish_description()` treats `pr_title=None` as "write only body", which drops the title line from `description.md`. Under the new contract, `None` means "do not change the existing title".
## Issue Context
`PRDescription.run()` now passes `pr_title=None` when `generate_ai_title` is false (default). For the local provider, the output file is the only persisted representation, so omitting the title effectively erases it.
## Fix Focus Areas
- pr_agent/git_providers/local_git_provider.py[112-116]
## Suggested fix
When `pr_title is None`, preserve the existing title for local output (e.g., use `self.pr.title`, or read the first line of the existing `description.md` if present) and write `f"{existing_title}\n{pr_body}"` instead of writing body-only.

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



Informational

3. Incorrect pr_title typing 🐞 Bug ⚙ Maintainability
Description
GitProvider.publish_description() is still typed as pr_title: str even though the contract now
explicitly allows None to mean “do not update the title.” This mismatch makes the interface
misleading and increases the chance of future provider implementations incorrectly assuming
pr_title is always a string.
Code

pr_agent/git_providers/git_provider.py[R169-172]

def publish_description(self, pr_title: str, pr_body: str):
+        # pr_title may be None, which means "leave the existing title unchanged"
+        # and update only the description. Implementations must not write the
+        # title in that case.
Evidence
The interface explicitly states pr_title may be None while keeping a str annotation, and
implementations now contain explicit None handling, demonstrating the behavioral contract has
changed beyond the type hints.

pr_agent/git_providers/git_provider.py[168-173]
pr_agent/git_providers/github_provider.py[360-366]

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 `publish_description` interface documents that `pr_title` may be `None`, but its type annotation remains `str`. This makes the public API inconsistent with the documented behavior and can mislead future implementations/callers.
## Issue Context
Providers are already implementing runtime `None` checks, so this is mostly an API clarity/static typing consistency issue.
## Fix Focus Areas
- pr_agent/git_providers/git_provider.py[168-173]
- pr_agent/git_providers/github_provider.py[360-366]
## Proposed fix
Update the abstract method signature (and provider overrides) to use `Optional[str]` (or `str | None` on 3.10+) for `pr_title`, and adjust any downstream signatures that forward the value so the typing contract matches the documented/actual behavior.

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


Grey Divider

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

Qodo Logo

Comment thread pr_agent/git_providers/bitbucket_server_provider.py Outdated
…a stale title

When pr_title is None the cached self.pr.title could be stale, so a title
edited during the describe run could still be reverted. Re-fetch the PR
first so the latest title, version and reviewers are used in the
replace-style update.

Refs: The-PR-Agent#2474
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 27903ad

…AI-generated

When pr_title is None the local provider wrote only the body, dropping the
title line from description.md. Use the provider's existing title
(get_pr_title) so the title line is preserved, matching the
leave-title-unchanged contract.

Refs: The-PR-Agent#2474
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@IsmaelMartinez

Copy link
Copy Markdown
Contributor Author

Addressed the new Local output drops title finding in d45ddc84: LocalGitProvider.publish_description now keeps the title line by using get_pr_title() when pr_title is None, so the title is preserved in description.md rather than dropped.

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

Copy link
Copy Markdown
Contributor

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

Keep only the non-obvious rationale (shared describe path, the abstract
publish_description contract, and the Bitbucket Server re-fetch); drop the
per-provider comments that just restate the guard.

Refs: The-PR-Agent#2474
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

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

@naorpeled naorpeled left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!
Thanks @IsmaelMartinez !

@naorpeled naorpeled merged commit 9f34d73 into The-PR-Agent:main Jun 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.

/describe overwrites a manually-edited PR title when generate_ai_title is false

2 participants