Preserve externally managed labels during synchronization#957
Preserve externally managed labels during synchronization#957loganrosen wants to merge 2 commits into
Conversation
Replace whole-set label writes with batched selective additions and removals so sync-labels only manages configured labels. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182
There was a problem hiding this comment.
Pull request overview
This PR updates the sync-labels implementation so label synchronization never rewrites or removes labels that are not present in the labeler configuration, preventing concurrent/user-added labels from being lost during a run (addresses #763).
Changes:
- Replace “set full label set” synchronization with selective operations: add missing configured labels via one REST batch (
issues.addLabels) and remove stale configured labels via one GraphQL mutation (removeLabelsFromLabelable). - Disable Octokit retry behavior for label additions to avoid masking ambiguous partial POST outcomes.
- Update docs/tests and remove the
lodash.isequaldependency (and bundled dist code).
Show a summary per file
| File | Description |
|---|---|
| src/labeler.ts | Switches sync-labels to selective add/remove to preserve unconfigured labels and reduce race risk. |
| src/api/remove-labels.ts | Adds GraphQL mutation helper for bulk removal of configured stale labels. |
| src/api/index.ts | Updates API exports to addLabels/removeLabels and removes setLabels. |
| src/api/add-labels.ts | Renames and changes implementation to REST addLabels, disabling retries. |
| README.md | Clarifies sync-labels behavior and output semantics when concurrent labels exist. |
| action.yml | Aligns sync-labels input description with new “configured-only removal” behavior. |
| package.json | Drops lodash.isequal and its types from dependencies. |
| package-lock.json | Removes lodash.isequal and related type packages from the lockfile. |
| dist/index.js | Updates the bundled action to reflect the new selective sync behavior and removed lodash code. |
| .licenses/npm/lodash.isequal.dep.yml | Removes the license metadata file for the deleted dependency. |
| tests/main.test.ts | Updates mocks/expectations for addLabels + GraphQL removal and adds new coverage for concurrent-label preservation. |
| tests/labeler.test.ts | Updates API mocks for new functions and adds coverage for removal-failure error reporting. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 10/12 changed files
- Comments generated: 0
- Review effort level: Low
|
Thanks for working on this and for investigating the non-atomicity issue. I spent some time testing this approach with repositories configured with a large number of labels, and I wanted to share a few observations that may be worth considering. In my testing,
Because of that, I'm not convinced that Since the labels have already been applied, In that sense, there is nothing to "heal"—the initial failure is a false negative. Given that
#497
introduced I've attached a summary of the workflow failure from my testing, highlighting the failed GitHub PR check and the corresponding 502 Server Error logs. I hope these findings are helpful in evaluating the trade-offs of this approach. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182
|
Thanks for reproducing this—you’re right that |
…ng and retries in label removal
…ndling in direct API calls and labeler execution path
…and address remaining concerns
…findings for PR actions#957 with cross-verification details
…cerns and risks associated with the two-call design
…ate risks and recommended README updates

Description:
sync-labelscurrently narrows its race window with a final label read, then replaces the complete label set. A concurrent label added after that read can still be lost.This restores the selective label-ownership isolation used by v4 before #497: matching configured labels are added in one REST batch, while stale configured labels are removed in one selective GraphQL mutation. It retains v6 matching, synchronization, and label-limit semantics without ever rewriting unconfigured labels. Addition retries are disabled; if a server error follows a committed write, the action verifies the requested labels once before deciding whether to surface the original failure.
Related issue:
Addresses #763.
Check list: