Repository navigation
feat: support assignee and label changes in issue update - #44
Closed
seraph-pixelperfect wants to merge 1 commit into
Closed
seraph-pixelperfect wants to merge 1 commit into
seraph-pixelperfect wants to merge 1 commit into
Conversation
Add --assignee <name|me>, --label <name> (repeatable), and --remove-label <name> (repeatable) to `issue update` (#24). - --assignee: "me" uses the viewer id from fetchViewer directly (no users() round trip for a write); any other name resolves via users(filter: { name: { eq } }) in the new resolveUserId, which fails loud on no match and on ambiguous display names (candidates listed). Re-assigning the current assignee is a field-level no-op. - --label/--remove-label: Linear's IssueUpdateInput.labelIds REPLACES the whole set, so the command computes the final set — current label ids (ISSUE_DETAIL_FIELDS now selects labels { nodes { id name } }) minus names matched by --remove-label (case-insensitive against the issue's own labels), unioned with --label names resolved via the exported resolveLabelIds. An empty computed set sends labelIds: [] explicitly (removing the last label works); a set identical to the current one is skipped, and an update where every field is a no-op reports "(no-op)" without mutating. - updateIssue (src/linear.ts) extends the omit-null input builder with assigneeId/labelIds; labelIds === [] is deliberately included. - Schema verified against @linear/sdk v90 generated documents: Query.users takes filter: UserFilter (UserFilter.name is a StringComparator with eq), User exposes id/name/email, and IssueUpdateInput has assigneeId?: String ("The identifier of the user to assign the issue to") and labelIds?: [String] ("The identifiers of the issue labels associated with this ticket"). - ISSUE_HELP documents the flags; SKILL.md regenerated (build:skill). Tests: test/issue-update-assignee-labels.test.ts (15 cases, stubbed fetch, no network) — "me" and named resolution, not-found/ambiguous users, label union, one-of-two and last-label removal (labelIds: []), the combined acceptance case, no-op paths, the nothing-to-update guard, and omit-null mutation documents.
This was referenced Aug 19, 2026
This was referenced Aug 20, 2026
Collaborator
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.
Closes #24.
Depends on #43.
What
issue updatelearns three new flags (all composable with the existing--state/--title/--priority/--description):--assignee <name|me>—"me"resolves to the authenticated viewer's id (viafetchViewer, which already returnsid name email— nousers()round trip needed for a write); any other name resolves throughusers(filter: { name: { eq } })in the newresolveUserId(src/linear.ts), which fails loud on no match and on ambiguous display names (candidates listed with emails, mirroringresolveStateIdByName's convention).--label <name>(repeatable) — added to the issue's current labels; names resolved workspace-wide via the now-exportedresolveLabelIds(delegates tofetchLabels).--remove-label <name>(repeatable) — removed from the issue's current labels, matched case-insensitively against the issue's own labels (no extra network round trip; removing a label the issue doesn't carry is a no-op).Because Linear's
IssueUpdateInput.labelIdsreplaces the whole set, the command layer computes the final set (current ids − removed ∪ added) and sends it wholesale. Two deliberate details:labelIds: []is sent explicitly — the omit-null input builder would otherwise skip the field and silently keep the label.issue: LIN-1 already up to date (no-op)and sends no mutation at all.The issue's current label ids come from extending
ISSUE_DETAIL_FIELDStolabels { nodes { id name } }— the update flow already fetches the issue, so this adds zero round trips; renderers read onlyname, so output is unchanged.Schema verification (source:
@linear/sdkv90.0.0 generated documents, vianpm pack)QueryUsersArgs(dist/index-zxW4m1xd.d.mts):userstakesfilter?: UserFilter(plus first/after/orderBy/…);UserFilter.nameis aStringComparatorexposingeq— sousers(filter: { name: { eq: $name } })is schema-valid.Usertype:id: ID,name: String,email: String— all three selected inresolveUserId(email is what makes the ambiguity error useful).IssueUpdateInput:assigneeId?: InputMaybe<String>("The identifier of the user to assign the issue to") andlabelIds?: InputMaybe<Array<String>>("The identifiers of the issue labels associated with this ticket" — replacement semantics).Before / after (stubbed variables, real code path)
issue update LIN-1 --assignee me --label bug— issue unassigned, no labels; vieweruser-viewer; workspace labelbug→lb-bug:Before this PR both flags were rejected:
unknown flag --assignee/unknown flag --label(exit 2).issue update LIN-1 --remove-label bug— issue carries exactly one label (bug→lb-bug):Also verified: named assignee (
UserByName→{"name":"Ada Lovelace"}→assigneeId:"user-ada"), and re-assigning the current assignee reportsissue: LIN-1 already up to date (no-op)with no mutation sent.Local gates (real output)
Baseline on the parent branch was 134 tests / 13 files; this adds
test/issue-update-assignee-labels.test.tswith 15 cases (stubbed fetch, no network):--assignee meand named resolution, not-found and ambiguous user errors, label union with existing labels, one-of-two and last-label removal, the combined acceptance case, both no-op paths, the nothing-to-update guard (now listing all seven flags, fires pre-network), blank flag values, omit-null mutation documents, and help coverage.pnpm run format:checkremains red on the same pre-existing 18 files (identical set to the parent branch; CI does not run it). No newly-flagged files; the new test file passes prettier.Stacked PR note
Stacked on #43 (stack: #37 → #38 → #39 → #40 → #41 → #42 → #43). This branch is based on the tip of
feat/labels-command(8cca1bc); parent commits are untouched. No CI checks appear on this PR —ci.ymlonly triggers on PRs targetingmain. CI runs when this PR is retargeted tomainafter the stack merges; the local gates above are the evidence in the meantime. Not retargeting.CI quota caveat: if checks do run later and appear as failures, verify before rerunning — GitHub Actions billing/spending-limit rejections look like check failures; check
gh run view <run-id>or the jobs API for a billing annotation, or an emptystepslist with a ~3-4s duration. If quota-blocked, treat the local output above as the evidence and don't retry.Ambiguities / judgment calls (flagged for review)
ISSUE_DETAIL_FIELDS(labels { nodes { id name } }) rather than re-fetching ids by name — zero extra round trips, exact ids instead of name matching. Side effect:issue view's query also selects label ids now (a few extra bytes; output unchanged).--stateno-op short-circuits the whole command even when other flags are present — left untouched as out of scope for Support assignee and label changes inissue update#24, but worth a follow-up issue if the inconsistency bothers you.resolveUserIdfails loud listingName <email>candidates rather than guessing.--assignee mebypasses the problem entirely.--labelnames are silently skipped (the issue says "reuseresolveLabelIds", whose existingissue createsemantics skip unknown names) — flagged in case reviewers want update to be stricter.addedLabelIds/removedLabelIdsalso exist onIssueUpdateInput(verified in the SDK types) and would avoid the full-set computation; I followed the issue spec's union/difference +labelIdsdesign, which also makes the remove-last-label case explicit.--labeland--remove-label: add wins (removal applies to the current set, then additions are unioned in).Identity note
Opened by an automated agent dispatch authenticated as
seraph-pixelperfect, submitted for human review — not self-approved. Merging (and retargeting) is a human decision.