fix(gitlab): preserve user-added labels#2484
Conversation
PR Summary by Qodofix(gitlab): preserve user-added labels during label publish/refresh
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. F541 f-strings in debug()
|
c323c58 to
e26403b
Compare
|
Code review by qodo was updated up to the latest commit e26403b |
e26403b to
ee9060d
Compare
|
Code review by qodo was updated up to the latest commit ee9060d |
…labels
PRReviewer.set_review_labels and similar callers do a read-modify-write
cycle on MR labels (read existing via get_pr_labels(update=True), filter
out our managed labels, add review-effort/security labels, write back via
publish_labels) specifically to preserve user-added labels. On the GitLab
provider both halves of that cycle were buggy, so any label a user added
after the webhook fired would be wiped on every /review.
Issue 1: GitLabProvider.get_pr_labels(update=False) ignored the update
flag and returned self.mr.labels from the snapshot cached at provider
construction. Callers requesting a fresh read silently got stale data,
so user-added labels were missing from current_labels and excluded from
the write.
Issue 2: GitLabProvider.publish_labels did:
self.mr.labels = list(set(pr_types))
self.mr.save()
which issues a full PUT on the labels array against that same stale
snapshot. Any concurrent addition during the read-modify-write window
is silently dropped.
This change fixes both halves:
- get_pr_labels(update=True) now actually refreshes via
self._get_merge_request() before reading. update=False keeps the
cached read so the hot path is unchanged. Refresh failure propagates
to the caller so a stale snapshot cannot corrupt the
read-modify-write cycle (see PRDescription change below).
- publish_labels refreshes the MR immediately before computing the
delta, then uses python-gitlab's add_labels / remove_labels
attributes (forwarded by python-gitlab to identically-named API
parameters on PUT /projects/:id/merge_requests/:merge_request_iid)
on save(). The server now applies an incremental set-diff, so
concurrent additions outside the diff are preserved.
- save() is wrapped in try/finally; both add_labels and remove_labels
are delattr'd in the finally block so a later self.mr.save() on the
same provider instance (e.g. publish_description) cannot resend the
prior label diff. Cleanup runs on save() failure too.
- publish_labels aborts on refresh failure (logs and returns without
calling save()) so we never publish from a stale snapshot.
- The refresh-failure warning logs only the exception type, not the
raw exception object. HTTP/SDK client errors can carry credentialed
URLs or headers in their message; logs are a security boundary.
PRDescription.run now isolates label-update failures: the label
refresh/publish block is wrapped in an inline try/except so that a
transient GitLab API error during get_pr_labels(update=True) (or a
publish_labels failure) does not abort the surrounding description
publish. Without this wrapper, the outer try/except in run() is wide
enough that a label-step exception would skip the description publish
too. PRReviewer.set_review_labels already wraps its
get_pr_labels(update=True) call in a try/except, so no change is
needed there. Other providers' get_pr_labels methods swallow
exceptions and return [], which is why callers were not previously
structured to expect exceptions here.
Tests:
New tests in tests/unittest/test_gitlab_provider.py cover:
- publish_labels: noop when sets are equal, add-only diff, remove-only
diff, user labels outside the diff are not touched, abort on refresh
failure (and the warning log redacts the exception body), diff-attr
cleanup on save() failure.
- get_pr_labels: no-update returns cached, update refreshes, update
propagates refresh failure.
New tests in tests/unittest/test_pr_description.py cover the
description publish flow remaining intact when:
- get_pr_labels(update=True) raises.
- publish_labels raises.
PYTHONPATH=. ./.venv/bin/pytest tests/unittest -q
1052 passed
Signed-off-by: Allen Shearin <allen.p.shearin@gmail.com>
ee9060d to
22facfb
Compare
|
Code review by qodo was updated up to the latest commit 22facfb |
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 in a single, focused change. 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, butarea/backendis gone.area/backendis preserved; only the agent-managed labels are updated.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 from the server, and the write becomes an incremental set-diff rather than a full overwrite.
get_pr_labels(update=True)now actually refreshesupdate=Falsekeeps the cached read, so callers that don't ask for a refresh see no behavior change. Refresh failure propagates to the caller —PRReviewer.set_review_labelsalready wraps itsget_pr_labels(update=True)call in atry/except, so a transient GitLab API error degrades into "skip the label update for this run" rather than corrupting the read-modify-write cycle.publish_labelsrefreshes, writes a diff, and cleans upKey changes from the previous implementation:
save().add_labels/remove_labels. python-gitlab forwards those attributes to identically-named parameters onPUT /projects/:id/merge_requests/:merge_request_iid(GitLab MR API). The server applies a set-diff rather than overwriting the array.try/finallyclears the transient diff attrs.add_labelsandremove_labelsaredelattr'd aftersave()(or on save failure) so they cannot leak into a subsequentself.mr.save()from an unrelated tool (e.g.,publish_description).pr_typesalready matches the server's label set, nosave()is issued at all.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.Tests
Added under
tests/unittest/test_gitlab_provider.py(10 new cases, all isolated to the two methods this PR touches):publish_labels:save()not called);save()not called);add_labels/remove_labels) are cleared even whensave()raises.get_pr_labels:update=Falsereturns the cached snapshot and does not refresh;update=Truerefreshes via_get_merge_request()and returns the fresh labels;update=Truepropagates refresh failures to the caller.Related
v0.34.x–v0.37.0).auto_review(or repeated manual/review) and any user-curated label workflow.