fix(issues): create --output json reports the labels it applied - #74
Open
FelixLisczyk wants to merge 3 commits into
Open
fix(issues): create --output json reports the labels it applied#74FelixLisczyk wants to merge 3 commits into
FelixLisczyk wants to merge 3 commits into
Conversation
The issueCreate mutation sent labelIds correctly, so Linear applied the labels, but the mutation's selection set never asked for `labels` back. The response therefore always carried `"labels": null`, even on a fully successful create. A scripted or agentic caller reading the create response to confirm label application got a false negative and had to pay an extra `issues get` round-trip to find out the labels were there all along. Two defects were in play, and both are fixed here: 1. The selection set omitted `labels`. It also omitted `priority`, `estimate`, `dueDate`, and `cycle` — every one of them a value the caller passes *into* create, so every one of them carried the identical "I set it, the response says null, did it work?" failure. All five are now requested, copying the shapes from GetIssue verbatim so the two selection sets read as siblings. `delegate` and `attachments` are deliberately left out: issueCreate cannot set a delegate, and a new issue has no attachments, so requesting them could never surface caller-supplied data. 2. The DTO layer rendered an empty collection as `null`, which is ambiguous between "this issue has none" and "this renderer does not report them" — exactly the confusion above. populateIssueBase now always allocates, so `labels`, `children`, and `attachments` render as `[]`. Delegate deliberately keeps `omitempty` and still vanishes when nil; an absent object is unambiguous in a way an absent collection is not, and the comment on populateIssueBase records that so the two are not later "harmonised" back together. This changes read-path JSON as well: `issues get|list` and `search` at detailed|full report `[]` where they reported `null`. Confining it to create would mean duplicating the DTO layer and making create's output structurally diverge from get's, which undercuts the parity this fix is for. It is a real if small break for consumers using has(), == null, or // defaults, rather than a cosmetic one. Tests are layered because no single layer covers this. The service-level tests build their own core.Issue via the mock, so they prove the DTO/formatter path but would stay green if the selection set regressed. The new httptest-backed client test captures the outgoing mutation body and is the only guard that fails on that regression — verified by removing the fields and watching it go red. Its assertions are anchored to whole lines, since a bare `labels` substring also matches `labelIds` in the mutation input and would pass against the unfixed code.
The previous commit made labels, children, and attachments render as an empty array rather than null. Comments were left rendering as null, which would emit `"labels": []` next to `"comments": null` in the same JSON object — reproducing, one field over, the exact ambiguity that commit set out to remove. Kept deliberately separate so it can be reverted on its own without disturbing the labels fix, since it touches a field the create defect never involved. One acknowledged asymmetry: create does not request comments at all, so a newly created issue now reports `"comments": []` for something the server was never asked about. That is the accepted cost of a uniform contract — the DTO describes the shape it renders, not the wire response it was built from — and the test records it.
The end-to-end create test claimed to cover the GraphQL selection set, but its fake server ignored the request and answered with a canned body that carried labels regardless — so deleting the selection set left it green. Its handler now asserts the captured mutation actually asks for labels. Also cover parent in the field-survival test, and anchor the LabelDTO and delegate leak assertions to their own fields instead of substring-scanning the whole document, so they cannot start failing on unrelated content. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBkx8wtrq2Mtb8N7jNerwZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
linear issues create --labels "Bug" --output jsonalways reported"labels": null, even though the labels were applied correctly.The write succeeded; only the response was wrong. A scripted or agentic caller that reads the create response to confirm label application gets a false negative, and has to pay an extra
issues getround-trip to discover the labels were there all along.Root causes
Two independent defects, both fixed here.
1. The
issueCreateselection set never asked forlabelsback.labelIdswas sent correctly in the mutation input — which is why Linear applied the labels — but theissue { … }selection set requested neitherlabelsnor four sibling fields the caller can also set:priority,estimate,dueDate,cycle. Each carried the identical "I set it, the response says null, did it work?" failure.All five are now requested, copying the shapes from
GetIssueverbatim so the two selection sets read as siblings.delegateandattachmentsare deliberately left out:issueCreatecannot set a delegate, and a new issue has no attachments, so requesting them could never surface caller-supplied data.2. The JSON DTO layer rendered an empty collection as
null. That is ambiguous between "this issue has none" and "this renderer does not report them" — the same confusion, one layer down.populateIssueBasenow always allocates, solabels,children, andattachmentsrender as[].delegatedeliberately keepsomitemptyand still vanishes when nil: an absent object unambiguously means "no delegate", whereas an absent or null collection is ambiguous. A comment onpopulateIssueBaserecords this so the two are not later "harmonised" back together.Behaviour change to read paths — please read
populateIssueBaseis shared, so this also changesissues get,issues list, andsearchat--format detailed|full: emptylabels,children,attachments, andcommentsnow render as[]instead ofnull.Consumers using
length,select, or truthiness tests are unaffected.jqexpressions along the lines ofhas("labels"),.labels == null, or.labels // "none"will behave differently.Confining the fix to
createwould mean duplicating the DTO layer and making create's output structurally diverge fromget's — undercutting the very parity this fix exists to establish. Text output is unchanged.Tests
Layered, because no single layer covers this bug:
pkg/linear/issues/client_create_test.go(new) — anhttptest-backed test that captures the outgoing mutation body. This is the only guard that fails if the selection-set fix is reverted; verified by removing the fields and watching it go red. Assertions are line-anchored, since a barelabelssubstring also matcheslabelIdsin the mutation input and would pass against the unfixed code. Also covers response mapping (including a label with aparent),success: false, and metadata extraction.internal/format/json_dtos_test.go(new) — raw-JSON assertions that empty collections marshal to[]and notnull, that a nil connection and a present-but-empty one both render[], thatLabelDTOstill dropscolor/parent, and that a nildelegatestays omitted.internal/service/issue_create_test.go— serialized-output assertions through the real formatter: one label, two labels, no labels, label resolution failure,CreateIssuefailure, and text mode unchanged.Service-level tests build their own
core.Issuevia the mock, so they prove the DTO path but would stay green on a selection-set regression — hence the client-layer test.make testpasses;go vet ./...is clean.Verified live
Against a real Linear team, comparing the released binary with this branch:
--labels "A,B"nullid+name--priority 2 --estimate 3null,null2,3--due 2026-12-31null"2026-12-31"id/namevsissues getnull[]ABC-123: <title>+ URLThe five-field selection set is valid against Linear's live schema.
Out of scope
issues updatehas the same gap in its client-layer selection set, but no--output/-oflag at all, so it is unreachable from the CLI. Adding JSON output there is a feature addition and needs to cover all three label modes (--labels,--add-labels,--remove-labels).LabelDTOwithcolor/parent. The mutation selects them (to stay identical to the read paths), but the DTO still drops them, so the output surface is unchanged.fragment IssueFields on Issuewould structurally prevent the drift, but rewriting seven live queries is disproportionate risk for a bug fix.