Skip to content

fix: stop the zone labeller from eating the PR size label - #204

Closed
qiuethan wants to merge 1 commit into
stagingfrom
qiuethan/fix-label-race
Closed

fix: stop the zone labeller from eating the PR size label#204
qiuethan wants to merge 1 commit into
stagingfrom
qiuethan/fix-label-race

Conversation

@qiuethan

Copy link
Copy Markdown
Collaborator

What this changes

zone-label.yml and pr-size-label.yml now share one concurrency group, so they serialise instead of interleaving.

Why

actions/labeler writes labels with setLabelsPUT /issues/{n}/labels, which replaces the whole label set rather than adding to it:

const preexistingLabels = pullRequest.data.labels.map(l => l.name);  // live read
const allLabels = new Set(preexistingLabels);
// ...add matched zone labels...
await api.setLabels(client, pullRequest.number, labelsToAdd);        // PUT = full replace

Read, compute, write back. Both label workflows fire on the same event, so anything one adds between the other's read and write is silently discarded — a textbook lost update.

This is not hypothetical. From PR #203's issue timeline:

23:50:36  labeled    size/l                             ← pr-size-label: addLabels
23:50:37  unlabeled  size/l                             ← labeler: setLabels
23:50:37  labeled    zone: docs, scripts, .github, root

It's a coin flip, not a permanent failure. When labeler happens to finish first, the size labeller's addLabels is additive and both survive — which is also why rerunning the job looked like it fixed itself. By then the zone labels already existed, so labeler's read included size/l.

The symptom is nasty precisely because nothing fails: both jobs report success, and the label just quietly isn't there.

The fix

concurrency:
  group: pr-labels-${{ github.event.pull_request.number }}
  cancel-in-progress: false

Identical in both files. Concurrency group names are repo-wide, so two different workflows sharing one serialise.

Once serialised, either order is correct:

Order Why it works
size → labeler labeler's live read now includes size/l, so setLabels writes it back
labeler → size addLabels is additive

cancel-in-progress: false is load-bearing — with true the two would cancel each other rather than queue, which is the opposite of what's needed. This replaces the narrower pr-size-${{ ... }} group added in #202, which guarded size-vs-size races but not size-vs-zone.

Why not just upgrade the action

I checked. v6 and v7 still call setLabels (v7 line 133). Only the unreleased main switched to addLabels/removeLabels. A version bump fixes nothing here, and v6+ additionally requires Node 24 runners.

Zone

.github · docs

Two zones: the workflows and the note describing the constraint.

How to verify

The mechanism is a race, so the honest check is behavioural — after this merges, open any PR and confirm it ends up with both a zone: * and a size/* label. Before the fix that succeeded roughly half the time.

Structurally verified here:

identical group: True  ['pr-labels-${{ github.event.pull_request.number }}', ...]
both cancel-in-progress false: True

The group strings must match exactly — that's the entire mechanism, so a typo in one file silently restores the bug. make labels still passes.

Checklist

  • Branched off staging and targeting staging.
  • Both workflow files parse; group strings asserted identical.
  • make labels clean.
  • Docs updated — CODE-OWNERSHIP.md now warns that any future PR labeller must join this group.
  • Python suites not run — this PR touches no Python.

Anything you're unsure about

cancel-in-progress: false means rapid pushes queue rather than cancel, so a busy PR may run the labellers a few times in sequence. That's the right trade for correctness — both are seconds long and idempotent — but it is a behaviour change from the true this had before.

🤖 Generated with Claude Code

actions/labeler writes with setLabels — PUT /issues/{n}/labels, which
REPLACES the whole label set rather than adding to it. It reads the PR's
labels, computes, and writes back, so anything added between that read and
that write is discarded. Both label workflows fire on the same event, so
they interleave.

Observed on PR #203, from the issue timeline:

  23:50:36  labeled    size/l
  23:50:37  unlabeled  size/l                    <- labeler's setLabels
  23:50:37  labeled    zone: docs, scripts, .github, root

It is a coin flip rather than a permanent failure: when labeler happens to
finish first, the size labeller's addLabels is additive and both survive.
That is also why a rerun looked like it fixed itself — by then the zone
labels existed, so labeler's read included size/l.

Both workflows now share one repo-wide concurrency group, which serialises
them. Either resulting order is correct: run after, and labeler's live read
already includes size/l; run before, and addLabels is additive.
cancel-in-progress must be false — with true the two would cancel each
other rather than queue, which is the opposite of what is needed.

Upgrading is not the fix: v6 and v7 still call setLabels. Only the
unreleased main branch switched to addLabels/removeLabels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/s < 50 lines changed zone: docs Owned by the docs zone (docs/CODE-OWNERSHIP.md) zone: .github Owned by the .github zone (docs/CODE-OWNERSHIP.md) and removed size/s < 50 lines changed labels Aug 17, 2026
@qiuethan qiuethan closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

zone: docs Owned by the docs zone (docs/CODE-OWNERSHIP.md) zone: .github Owned by the .github zone (docs/CODE-OWNERSHIP.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant