Repository navigation
Description
Describe the bug
When issue_write (method: update) sets or clears org issue field values, updateIssue in pkg/github/issues.go:
- reads all current field values over GraphQL (
fetchExistingIssueFieldValues,issueFieldValues(first: 25)), - merges the requested change into that snapshot (
mergeIssueFieldValues), - sends the whole merged set in the issue PATCH (
issueRequest.IssueFieldValues→patchIssue).
The issue PATCH replaces the whole field set, and nothing checks that the snapshot is still current. When two updates to the same issue overlap, each one writes back the other field's old value, so whichever finishes last reverts the other's change. Both calls report success.
Deletes hit the same problem: deleting one of several fields PATCHes the remaining snapshot back.
A smaller, separate problem: the read takes only the first 25 values (first: 25), so on an issue with more than 25 values, the rest are not in the merged set.
Affected version
Reproduced against v2.1.0 (commit 4bcdcdf8). The same code is on main at 99d6d6503a7b2a133e2f8a595d6d0ba24b4086d3.
Steps to reproduce the behavior
- In an org with issue fields, take an issue with a single-select field
Priorityand a date fieldTarget date, both set. - Send two
issue_writecalls at the same time against that issue:- A:
{"method":"update","owner":"<org>","repo":"<repo>","issue_number":N,"issue_fields":[{"field_name":"Priority","field_option_name":"P2"}]} - B:
{"method":"update","owner":"<org>","repo":"<repo>","issue_number":N,"issue_fields":[{"field_name":"Target date","value":"2026-12-01"}]}
- A:
- Read the issue's field values back. Run this several times.
Expected vs actual behavior
Expected: both changes stay: Priority = P2 and Target date = 2026-12-01.
Actual: often only one change survives. The call that finishes last PATCHes back the other field's old value.
Observed: 19 of 30 concurrent pairs lost an update, over 3 runs. As a control, the same pairs sent as per-field GraphQL setIssueFieldValue mutations (the path the feature-flagged granular set_issue_fields tool uses) lost 0 of 28.
Suggested fix: never send field values in the issue PATCH. Send only the requested values to the additive POST /repos/{owner}/{repo}/issues/{issue_number}/issue-field-values endpoint, and clear fields with the per-field DELETE …/issue-field-values/{field_id} that updateIssue already uses (treating 404 as already cleared). This also drops the extra GraphQL read and the 25-value cap on this path. Another option is to reuse the per-field GraphQL setIssueFieldValue path from set_issue_fields. A patch for the REST option is ready if that helps.
Logs
Not applicable. A deterministic unit test shows the stale snapshot being written back. On main it fails with this PATCH body after setting only Priority (101):
{"issue_field_values":[{"field_id":101,"value":"P2"},{"field_id":202,"value":"2026-01-01"}]}
Found during an independent audit of v2.1.0 on a throwaway org and private test repo. Prepared with AI assistance (Claude Code); reproduced by two independent test runs before filing.
Activity
rafe-walker commentedon Nov 9, 2026
Before opening a PR, I'd like to check the approach with you, because the fix runs into a design point raised on #2755.
On #2755 the preference was for
issue_writeto "call the updateIssue endpoint only, and do all the mutations for an issue in one operation." I don't think that can be made safe here. The issue PATCH has set semantics forissue_field_values, so any single PATCH that carries fields has to carry all of them, which means reading them first, and the gap between that read and the write is the race. Without a precondition on the PATCH (an ETag or version), one call can't avoid it.I have a patch that keeps the issue PATCH but takes fields out of it:
POST /repos/{owner}/{repo}/issues/{issue_number}/issue-field-valuesDELETE …/issue-field-values/{field_id}thatupdateIssuealready usesfirst: 25cap on this path)Tested live against a build of current
mainwith the patch, same setup as the report:The trade-off is that a call changing both issue properties and fields is now two requests instead of one. To keep the common failure (an invalid field value) all-or-nothing, the field writes can go before the PATCH, and if anything partially applies, the error says exactly what was saved.
Two other things worth knowing:
DELETEreturning 404 is treated as already cleared, which could also mask a permission or feature-flag 404Would you accept this direction? If you'd rather keep everything in one PATCH (e.g. if the endpoint can take a precondition), or route fields through the GraphQL
setIssueFieldValuemutation thatset_issue_fieldsuses, I'm happy to rework it that way before opening the PR.Prepared with AI assistance (Claude Code).