fix(gitlab): preserve user-added labels in publish_labels and get_pr_…#2483
fix(gitlab): preserve user-added labels in publish_labels and get_pr_…#2483ashearin wants to merge 4 commits into
Conversation
PR Summary by QodoFix GitLab label publishing to preserve user-added MR labels
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Code Review by Qodo
1. Settings custom_labels not restored
|
Two issues raised by automated review on The-PR-Agent#2483: 1. (Rule violation, reliability) On _get_merge_request() refresh failure, the prior best-effort behavior could fall back to a cached self.mr snapshot and still call save() with a stale diff, defeating the point of the refresh. Both publish_labels and get_pr_labels(update=True) now treat a refresh failure as a hard abort: - publish_labels logs a warning and returns without calling save(); server state is left untouched. - get_pr_labels(update=True) lets the exception propagate. The existing caller (PRReviewer.set_review_labels) already wraps the call in a broad try/except, so the failure degrades into 'skip the label update for this run' rather than corrupting the cycle with stale labels. 2. (Bug, correctness) publish_labels was setting self.mr.add_labels / self.mr.remove_labels but never clearing them, so a later self.mr.save() on the same provider instance (e.g. publish_description right after publish_labels in the /describe flow) would resend the prior label diff alongside the unrelated update. The save() is now wrapped in try/finally; both add_labels and remove_labels are delattr()'d in the finally block. Cleanup runs on save() failure too, so a transient save error cannot leave diff fields on the shared object. Tests: - New tests/unittest/test_gitlab_provider.py cases covering: noop when sets are equal, add-only diff, remove-only diff, user labels outside the diff are not touched, abort on refresh failure, diff-attr cleanup on save() failure, and the three get_pr_labels paths (no-update, update success, update failure propagates). - All 32 tests in test_gitlab_provider.py pass; the full unit suite (PYTHONPATH=. ./.venv/bin/pytest tests/unittest -v) reports 1050 passed, 1 skipped, 3 xfailed.
|
Code review by qodo was updated up to the latest commit 594c97f |
Follow-up to review on The-PR-Agent#2483. After the prior commit made get_pr_labels(update=True) propagate refresh failures, the PRDescription.run flow became fragile: its outer try/except (lines 96-200) is wrapped around the entire publish path, so a transient GitLab API blip during get_pr_labels(update=True) at line 160 would abort the run, skipping the description publish, not just the label update. Other providers (e.g. GithubProvider.get_pr_labels) treat label-fetch failures as non-fatal, so callers were not written to expect exceptions here. For /describe, the description itself is the primary artifact; the label update must not block it. Changes: - Extract the label refresh/publish block from PRDescription.run into a new helper PRDescription._safe_publish_labels(pr_labels). The helper wraps get_pr_labels(update=True) + publish_labels in a broad try/except that logs a warning and returns, leaving the surrounding description publish unaffected. - PRDescription.run now invokes self._safe_publish_labels(pr_labels) in place of the inline block. Tests added in test_pr_description_output_core.py: - test_skips_publish_when_refresh_raises: refresh failure does not propagate, publish_labels is not called. - test_skips_publish_when_publish_raises: publish_labels failure does not propagate. - test_publishes_when_labels_differ: happy path still publishes the merged set. - test_no_publish_when_labels_unchanged: no-op publish when the merged set matches the server set. PRReviewer.set_review_labels already wraps its get_pr_labels(update=True) call in a narrow try/except, so no change is needed there. PYTHONPATH=. ./.venv/bin/pytest tests/unittest -q reports 1054 passed (up from 1050).
|
Code review by qodo was updated up to the latest commit 204d145 |
|
Code review by qodo was updated up to the latest commit ff0ce9c |
ff0ce9c to
bb8ae12
Compare
…labels Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
|
Code review by qodo was updated up to the latest commit bb8ae12 |
bb8ae12 to
d2786f5
Compare
|
Code review by qodo was updated up to the latest commit d2786f5 |
- Add GitProvider.publish_managed_labels with a default implementation that preserves existing read-modify-write semantics for non-GitLab providers (no behavior change for GitHub, Bitbucket, Gitea, etc.). - Override in GitLabProvider to refresh the MR exactly once and compute both the user-label filter and the add/remove diff against that single snapshot. This closes the cross-method race where a caller's get_pr_labels(update=True) and publish_labels' internal refresh produced two snapshots, allowing a label added between them to be removed via current - desired. - /describe (_safe_publish_labels) and /review (set_review_labels) now call publish_managed_labels with a managed-label predicate instead of doing their own pre-read + manual merge + publish_labels. - Refactor publish_labels to share _publish_labels_against(mr, desired) with publish_managed_labels so the two write paths cannot drift. - _safe_publish_labels now catches a narrow set of expected provider/network errors (ConnectionError, TimeoutError, OSError, ValueError, KeyError, AttributeError) instead of bare Exception, so programmer errors surface to /describe's outer handler. - Strengthen publish_labels tests to capture add_labels / remove_labels at save-time via a save() side_effect and assert the exact comma-separated wire payload. - Add six tests for publish_managed_labels covering single-refresh, user-label preservation, no-op when nothing changes, removal of all managed labels when desired is empty, abort on refresh failure, and diff-attr cleanup on save() failure. Addresses Qodo review items #1 (broad except in _safe_publish_labels), The-PR-Agent#2 (cross-method label race), and The-PR-Agent#3 (weak test payload assertions) on PR The-PR-Agent#2483. Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
|
Code review by qodo was updated up to the latest commit a7a8cad |
Match the repo's double-quote convention (AGENTS.md) for the two managed-label prefix strings in _is_review_managed. No behavior change. Addresses Qodo inline review on PR The-PR-Agent#2483 (The-PR-Agent#2483 (comment)). Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
|
Code review by qodo was updated up to the latest commit 36ec454 |
…h failures via typed exception Addresses Qodo review items The-PR-Agent#2, The-PR-Agent#3, The-PR-Agent#4 on PR The-PR-Agent#2483. The-PR-Agent#2 Hardcoded managed_default label set - Add is_describe_managed_label() and _describe_managed_label_names() in pr_agent.algo.utils as the single source of truth for the /describe-managed namespace. - Rewrite get_user_labels() to defer to the same helper so the inclusion and exclusion rules can never drift. The-PR-Agent#3 Default label not managed (Bug fix with tests) - When enable_custom_labels=True and no custom_labels are configured, set_custom_labels injects defaults that include 'Bug fix with tests'. is_describe_managed_label now classifies that label as managed so /describe can remove it once it is no longer recommended. - This also fixes a pre-existing bug in get_user_labels where 'Bug fix with tests' was misclassified as a user label. The-PR-Agent#4 Refresh failure misreported - Add LabelRefreshError in pr_agent.git_providers.git_provider so callers can distinguish a refresh failure from a genuine no-op. - GitProvider.publish_managed_labels (default impl) and GitLabProvider.publish_managed_labels now raise LabelRefreshError on pre-write refresh failure instead of returning None. - PRDescription._safe_publish_labels and PRReviewer.set_review_labels catch LabelRefreshError specifically and log a warning that names the failure, instead of mis-logging it as 'labels unchanged' / 'already set'. #1, The-PR-Agent#5, The-PR-Agent#6, The-PR-Agent#7 from Qodo's review are already addressed in earlier commits on this branch (36ec454, a7a8cad); Qodo's review listing is pinned to PR-level diff hunks so they appear in the report against older revisions. Tests: - New TestDescribeManagedLabelNamespace locks the predicate behavior: default-set always managed, Bug fix with tests managed under the custom-labels default-injection path, and is_describe_managed_label / get_user_labels stay disjoint. - TestSafePublishLabels gains test_predicate_includes_bug_fix_with_tests_when_custom_labels_default and test_logs_distinctly_on_refresh_failure. - test_pr_reviewer_core gains test_set_review_labels_logs_distinctly_on_refresh_failure. - test_publish_managed_labels_aborts_when_refresh_fails replaced with test_publish_managed_labels_raises_when_refresh_fails. - Full unit suite: 1066 passed, 1 skipped, 3 xfailed (was 1060). Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
| def _with_settings(self, enable_custom_labels, custom_labels): | ||
| from pr_agent.config_loader import get_settings | ||
| settings = get_settings() | ||
| snap = ( | ||
| settings.config.get("enable_custom_labels", False), | ||
| settings.get("custom_labels", None), | ||
| ) | ||
| settings.config.enable_custom_labels = enable_custom_labels | ||
| if custom_labels is not None: | ||
| settings.set("custom_labels", custom_labels) | ||
| return settings, snap | ||
|
|
||
| def _restore(self, settings, snap): | ||
| original_enable, original_custom = snap | ||
| settings.config.enable_custom_labels = original_enable | ||
| if original_custom is not None: | ||
| settings.set("custom_labels", original_custom) | ||
|
|
There was a problem hiding this comment.
1. Settings custom_labels not restored 📘 Rule violation ☼ Reliability
The new test helper mutates the global Dynaconf custom_labels setting but only restores it when the original value is not None, which can leak state across tests and cause nondeterministic failures.
Agent Prompt
## Issue description
`TestDescribeManagedLabelNamespace._with_settings()` can change `settings.custom_labels`, but `_restore()` only restores it when the original value is not `None`. If the original value was `None` and the test sets it to `[]` (or another value), that mutated global/module state can leak into subsequent tests.
## Issue Context
This violates the requirement to snapshot and restore mutated global/module state in tests/requests to avoid flakiness.
## Fix Focus Areas
- tests/unittest/test_pr_description_output_core.py[629-646]
## Suggested approach
- Track whether `_with_settings()` actually changed `custom_labels` (e.g., store a boolean) and always restore it.
- If the original was `None`, explicitly restore to `None` (or delete/reset the key in a Dynaconf-safe way) rather than leaving the test-written value in place.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| variables["labels_minimal_to_labels_dict"] = labels_minimal_to_labels_dict | ||
|
|
||
| def _describe_managed_label_names(): | ||
| """Return the lowercase set of label names that ``/describe`` manages. |
There was a problem hiding this comment.
2. Missing blank-line separation 🐞 Bug ⚙ Maintainability
pr_agent/algo/utils.py defines _describe_managed_label_names() immediately after set_custom_labels() without the required two blank lines, which violates Ruff’s enabled E (pycodestyle) rules and can fail linting/CI.
Agent Prompt
### Issue description
Ruff is configured to lint pycodestyle `E` rules, which includes `E305` (expected 2 blank lines after class/function). In `pr_agent/algo/utils.py`, `_describe_managed_label_names()` starts immediately after `set_custom_labels()` with insufficient blank-line separation, which can break CI.
### Issue Context
The repository enables Ruff with `lint.select` including `E`.
### Fix Focus Areas
- pr_agent/algo/utils.py[964-970]
- pyproject.toml[52-75]
### How to fix
Insert the missing blank line(s) so there are **two** blank lines between the end of `set_custom_labels` and the start of `_describe_managed_label_names`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 3949680 |
fix(gitlab): preserve user-added labels in
publish_labelsandget_pr_labelsSummary
The GitLab provider drops user-added labels every time PR-Agent publishes a label set (most visibly during
/review). Two compounding bugs inGitLabProvidercause a read-modify-write race that wipes any label a user added after the webhook fired.This PR fixes both bugs on the GitLab provider. The hot path (no refresh requested, no label changes needed) is unchanged.
What users see
It only happens on GitLab. GitHub and other providers are unaffected because the bug is provider-local.
Reproduction
auto_reviewenabled (or trigger/reviewmanually).Review effort N/5and possiblyPossible security concernlabels).area/backend)./reviewagain, or push a new commit soauto_reviewfires.Review effortlabel is updated, but yourarea/backendlabel is gone.Root cause
PRReviewer.set_review_labelsdoes a read-modify-write cycle specifically to preserve user-added labels:Both halves of that cycle are broken on the GitLab provider.
Bug 1 —
get_pr_labels(update=True)ignores the flagThe
updateargument is unused; the method always returnsself.mr.labelsfrom the snapshot cached at provider construction. A label the user added after the webhook fired is missing fromcurrent_labels, so the filter step drops it out ofnew_labels.Bug 2 —
publish_labelsdoes a full PUT against the same stale snapshotSetting
self.mr.labelsand callingsave()translates to a full PUT on the labels array — the server overwrites whatever it has withpr_types. Any label added during the read-modify-write window (or any label missed because of Bug 1) is wiped.Fix
Both halves of the cycle now refresh, and the write becomes an incremental set-diff rather than a full overwrite.
get_pr_labels(update=True)now actually refreshesupdate=Falsekeeps the existing cached read, so callers that don't ask for a refresh see no behavior change.publish_labelsrefreshes and writes a diffWhy
add_labels/remove_labelsinstead oflabels:add_labelsandremove_labelsquery parameters onPUT /projects/:id/merge_requests/:merge_request_iid(see the GitLab MR API).Failure modes
Hot path
to_addandto_removeare both empty → the method returns without callingsave(). No-op publishes are now actually no-ops.What is NOT changed
GithubProvider,BitbucketProvider,BitbucketServerProvider,AzureDevopsProvider,GiteaProvider,GerritProvider,CodeCommitProvider,LocalGitProvider).publish_labelsandget_pr_labels.get_pr_labels(update=False)(still returns the cached snapshot).set_review_labels,PRDescription,PRGenerateLabels, etc. — they continue to call the same provider methods with the same arguments.Testing
Manual testing against a self-managed GitLab instance:
/reviewrun.area/backend./reviewagain.area/backendis gone after the second review.area/backendis preserved; only the agent'sReview effortlabel is updated.Edge cases verified manually:
pr_typesequals the current label set → method returns early, nosave()call.self.mr.labelsisNone→ handled bycurrent = set(self.mr.labels or [])./reviewexecution (between refresh andsave()) → preserved by the server because we only push the diff, not the whole array.I did not add automated tests in this PR — the existing
tests/unittest/test_gitlab_provider.pydoes not exercisepublish_labelsand adding a meaningful test would require either apython-gitlabmock with attribute-side-effect tracking or a live test environment. Happy to add tests in a follow-up if maintainers want them.Related
v0.34.x–v0.37.0).auto_review(or repeated manual/review) and any user-curated label workflow.