Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/docs/installation/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,13 @@ If you encounter rate limiting:
pull-requests: write
contents: write
```
If you cannot grant `contents: write`, set `config.restricted_mode = true` in your configuration. In that case you only need:
```yaml
permissions:
issues: write
pull-requests: write
```
See the [Restricted Mode guide](../usage-guide/additional_configurations.md#restricted-mode) for details.

**Error: "Invalid JSON format"**

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/usage-guide/additional_configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,23 @@ ignore_ticket_labels = ["ignore-compliance", "skip-review", "wont-fix"]
```

Where `ignore_ticket_labels` is a list of label names that should be ignored during ticket analysis.

### Restricted Mode

When running PR-Agent with limited GitHub/GitLab permissions, set `restricted_mode` to `true` to gracefully skip operations that require elevated access (e.g., pushing changelog changes):

```toml
[config]
restricted_mode = true
```

With restricted mode, the minimum workflow permissions are:

```yaml
permissions:
issues: write
pull-requests: write
contents: read # or just omit contents (defaults to read)
```

`contents: write` is not needed — any tool needing it (`/update_changelog` with `push_changelog_changes=true`) will skip the operation and post a clear comment instead of failing with a 403. All other tools (`/review`, `/describe`, `/improve`, etc.) continue to work normally.
2 changes: 2 additions & 0 deletions pr_agent/git_providers/bitbucket_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'publish_inline_comments', 'get_labels', 'gfm_markdown',
'publish_file_comments']:
return False
if capability == "push_code" and get_settings().config.restricted_mode:
return False
return True

def set_pr(self, pr_url: str):
Expand Down
2 changes: 2 additions & 0 deletions pr_agent/git_providers/github_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def get_incremental_commits(self, incremental=IncrementalPR(False)):
self._get_incremental_commits()

def is_supported(self, capability: str) -> bool:
if capability == "push_code" and get_settings().config.restricted_mode:
return False
return True

def _get_owner_and_repo_path(self, given_url: str) -> str:
Expand Down
2 changes: 2 additions & 0 deletions pr_agent/git_providers/gitlab_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments',
'publish_file_comments']: # gfm_markdown is supported in gitlab !
return False
if capability == "push_code" and get_settings().config.restricted_mode:
return False
return True

def _get_project_path_from_pr_or_issue_url(self, pr_or_issue_url: str) -> str:
Expand Down
1 change: 1 addition & 0 deletions pr_agent/settings/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created
ignore_repositories = [] # a list of regular expressions of repository full names (e.g. "org/repo") to ignore from PR agent processing
ignore_language_framework = [] # a list of code-generation languages or frameworks (e.g. 'protobuf', 'go_gen') whose auto-generated source files will be excluded from analysis
#
restricted_mode = false # when true, skip operations that require elevated permissions (e.g. pushing code to the repository)
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
enable_ai_metadata = false # will enable adding ai metadata
reasoning_effort = "medium" # "none", "minimal", "low", "medium", "high", "xhigh"
Expand Down
48 changes: 32 additions & 16 deletions pr_agent/tools/pr_update_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,29 @@ class PRUpdateChangelog:
def __init__(self, pr_url: str, cli_mode=False, args=None, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):

self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self.commit_changelog = get_settings().pr_update_changelog.push_changelog_changes
self._get_changelog_file() # self.changelog_file_str

# Skip expensive provider reads when push is disabled or restricted
self.push_changelog_changes = get_settings().pr_update_changelog.push_changelog_changes
if self.push_changelog_changes:
if not hasattr(self.git_provider, "create_or_update_pr_file"):
self._skip_push = "not supported"
elif not self.git_provider.is_supported("push_code"):
self._skip_push = "restricted"
else:
self._skip_push = None
else:
self._skip_push = None

if self._skip_push:
self.main_language = None
self.changelog_file_str = ""
else:
self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self._get_changelog_file() # self.changelog_file_str

self.commit_changelog = self.push_changelog_changes

self.ai_handler = ai_handler()
self.ai_handler.main_pr_language = self.main_language
Expand All @@ -42,7 +60,7 @@ def __init__(self, pr_url: str, cli_mode=False, args=None, ai_handler: partial[B
"language": self.main_language,
"diff": "", # empty diff for initial calculation
"pr_link": "",
"changelog_file_str": self.changelog_file_str,
"changelog_file_str": self.changelog_file_str if not self._skip_push else "",
"today": date.today(),
"extra_instructions": get_settings().pr_update_changelog.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(),
Expand All @@ -54,23 +72,21 @@ def __init__(self, pr_url: str, cli_mode=False, args=None, ai_handler: partial[B

async def run(self):
get_logger().info('Updating the changelog...')
relevant_configs = {'pr_update_changelog': dict(get_settings().pr_update_changelog),
'config': dict(get_settings().config)}
get_logger().debug("Relevant configs", artifacts=relevant_configs)

# check if the git provider supports pushing changelog changes
if get_settings().pr_update_changelog.push_changelog_changes and not hasattr(
self.git_provider, "create_or_update_pr_file"
):
get_logger().error(
"Pushing changelog changes is not currently supported for this code platform"
)
if self._skip_push:
reason = "not supported" if self._skip_push == "not supported" else "restricted by configuration"
get_logger().error(f"Pushing changelog changes is {reason}")
if get_settings().config.publish_output:
self.git_provider.publish_comment(
"Pushing changelog changes is not currently supported for this code platform"
f"Pushing changelog changes is {reason}"
)
return

relevant_configs = {'pr_update_changelog': dict(get_settings().pr_update_changelog),
'config': dict(get_settings().config)}
get_logger().debug("Relevant configs", artifacts=relevant_configs)
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated

if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)

Expand Down
24 changes: 12 additions & 12 deletions tests/unittest/test_pr_update_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,22 +139,22 @@ def test_prepare_changelog_update_no_commit(self, changelog_tool):
assert "to commit the new content" in answer

@pytest.mark.asyncio
async def test_run_without_push_support(self, changelog_tool, mock_git_provider):
async def test_run_without_push_support(self, mock_git_provider, mock_ai_handler):
"""Test running changelog update when git provider doesn't support pushing."""
# Arrange
delattr(mock_git_provider, 'create_or_update_pr_file') # Remove the method
changelog_tool.commit_changelog = True

with patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings:
delattr(mock_git_provider, 'create_or_update_pr_file')
with patch('pr_agent.tools.pr_update_changelog.get_git_provider', return_value=lambda url: mock_git_provider), \
patch('pr_agent.tools.pr_update_changelog.get_settings') as mock_settings:
mock_settings.return_value.pr_update_changelog.push_changelog_changes = True
mock_settings.return_value.config.publish_output = True

# Act
await changelog_tool.run()

# Assert
mock_settings.return_value.pr_update_changelog.extra_instructions = ""
mock_settings.return_value.pr_update_changelog_prompt.system = ""
mock_settings.return_value.pr_update_changelog_prompt.user = ""
tool = PRUpdateChangelog("https://example.com/pr/123")

await tool.run()

mock_git_provider.publish_comment.assert_called_once()
assert "not currently supported" in str(mock_git_provider.publish_comment.call_args)
assert "not supported" in str(mock_git_provider.publish_comment.call_args)

@pytest.mark.asyncio
async def test_run_with_push_support(self, changelog_tool, mock_git_provider):
Expand Down
Loading