Skip to content

Preserve externally managed labels during synchronization#957

Open
loganrosen wants to merge 2 commits into
actions:mainfrom
loganrosen:loganrosen-fix-label-ownership-sync
Open

Preserve externally managed labels during synchronization#957
loganrosen wants to merge 2 commits into
actions:mainfrom
loganrosen:loganrosen-fix-label-ownership-sync

Conversation

@loganrosen

@loganrosen loganrosen commented Jul 20, 2026

Copy link
Copy Markdown

Description:
sync-labels currently 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:

  • Mark if documentation changes are required.
  • Mark if tests were added or updated to cover the changes.

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
Copilot AI review requested due to automatic review settings July 20, 2026 16:55
@loganrosen
loganrosen requested a review from a team as a code owner July 20, 2026 16:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.isequal dependency (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

@chiranjib-swain chiranjib-swain self-assigned this Jul 22, 2026
@chiranjib-swain

Copy link
Copy Markdown
Contributor

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, addLabels (POST) became unreliable as the number of labels increased. For example, I observed:

  • 502 Bad Gateway when attempting to add 75 labels (and in some 100-label scenarios).
  • In the 502 case, GitHub had already committed the complete label update server-side, but still returned a 502 to the client. From the action's perspective, this is indistinguishable from a genuine write failure, so the workflow reports a failure even though the desired state has already been reached.

Because of that, I'm not convinced that retries: 0 fully addresses the problem. While it avoids retrying a non-idempotent request, it also means users receive a failed workflow for an operation that may have already completed successfully.

Since the labels have already been applied, newLabels is empty on the next workflow run, so the action succeeds without performing any work. However, this only happens if another workflow run is triggered (for example, by pushing a new commit or manually re-running the workflow). Otherwise, the PR continues to display the failed check even though the desired state has already been reached.

In that sense, there is nothing to "heal"—the initial failure is a false negative.


Given that #497 introduced setLabels primarily to enforce the 100-label limit—and that an effective side-effect was avoiding these POST failure modes—reverting to addLabels reintroduces the same reliability problems documented in #561. I think it's worth considering whether this trade-off is appropriate for repositories with a large number of configured labels.


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.

Workflow failure summary

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182
@loganrosen

Copy link
Copy Markdown
Author

Thanks for reproducing this—you’re right that retries: 0 alone still leaves a false-negative failure when the POST commits before returning 502. I’ve added read-after-error reconciliation: only after a 5xx, the action fetches the current labels once with retries disabled and treats the addition as successful only if every requested label is present. If any label is missing or verification fails, it preserves the original POST error. The normal path remains one request, with no blind POST retry. Regression coverage now includes the committed-502, incomplete-write, verification-failure, and non-5xx cases.

chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
…ndling in direct API calls and labeler execution path
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 23, 2026
…cerns and risks associated with the two-call design
chiranjib-swain added a commit to chiranjib-swain/labeler that referenced this pull request Jul 24, 2026
…ate risks and recommended README updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants