Skip to content

Commit d762f32

Browse files
bougymanclaude
andauthored
feat(issue): add --description option to lc issue update (#141)
## Summary - Adds `lc issue update --description TEXT` (short: `-d TEXT`) to update an issue's body text via Linear's `issueUpdate` mutation - Supports `--description -` to open an editor (consistent with `issue create` and other long-form inputs) - Follows existing update-option precedence order (applied after `--project`, before the comment-only fallback) - Preserves multiline Markdown descriptions without truncation or newline loss (handled by `WhatFor.description_for/1`) ## Changes - `app/lib/linear_cli/linear/issue.ex`: New `update :update_description` Ash action + `LinearCli.Linear.Issue.Update.UpdateDescription` ManualUpdate module - `app/lib/linear_cli/linear.ex`: New `update_issue_description/2` domain code interface - `app/lib/linear_cli/cli/issue_helpers.ex`: New `update_description/2` helper + `:description` clause in `dispatch_update/2` - `app/lib/linear_cli/cli/commands.ex`: Pass `description:` in `issue_update/1`'s `update_opts` (via `Map.get` to handle partial option maps in tests) - `app/lib/linear_cli/cli.ex`: `--description` / `-d` option on `issue update` subcommand ## Test plan - [ ] 2 new action-layer tests for `update_issue_description/2` in `issue_test.exs` - [ ] 3 new helper tests for `update_description/2` and `dispatch_update` in `issue_helpers_test.exs` - [ ] 2 new CLI integration tests for `--description` and `-d` in `issue_commands_test.exs` - [ ] All 7 new tests pass; pre-existing `LinearCli.GitTest` environment failures are unchanged Fixes https://linear.app/cryptokairos/issue/CRY-53/support-updating-issue-descriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9e5d7e0 commit d762f32

8 files changed

Lines changed: 180 additions & 0 deletions

File tree

‎app/lib/linear_cli/cli.ex‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,11 @@ defmodule LinearCli.CLI do
661661
long: "--comment",
662662
help: "Comment to add to the issue. - open an editor"
663663
],
664+
description: [
665+
short: "-d",
666+
long: "--description",
667+
help: "Update the issue description. - to open an editor"
668+
],
664669
project: [
665670
short: "-p",
666671
long: "--project",

‎app/lib/linear_cli/cli/commands.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ defmodule LinearCli.CLI.Commands do
457457
Linear.issues(%{ids: Enum.map(issue_ids, &IssueHelpers.expand_issue_id/1)}) do
458458
update_opts = [
459459
comment: options.comment,
460+
description: Map.get(options, :description),
460461
project: options.project,
461462
cancel: flags.cancel,
462463
close: flags.close,

‎app/lib/linear_cli/cli/issue_helpers.ex‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ defmodule LinearCli.CLI.IssueHelpers do
301301
end
302302
end
303303

304+
@doc """
305+
Updates `issue`'s description to `description_input`, resolving it (asking,
306+
or opening an editor, if not already given - via
307+
`LinearCli.CLI.WhatFor.description_for/1`) first.
308+
"""
309+
@spec update_description(%Linear.Issue{}, String.t() | nil) ::
310+
{:ok, %Linear.Issue{}} | {:error, term()}
311+
def update_description(issue, description_input) do
312+
description = WhatFor.description_for(description_input)
313+
314+
case Linear.update_issue_description(issue, description) do
315+
{:ok, updated} ->
316+
Prompt.ok("#{issue.identifier} description updated")
317+
{:ok, updated}
318+
319+
{:error, reason} ->
320+
{:error, reason}
321+
end
322+
end
323+
304324
@doc """
305325
Dispatches an issue update per whichever of `opts`' keys is set, in Ruby's
306326
exact precedence order:
@@ -341,6 +361,7 @@ defmodule LinearCli.CLI.IssueHelpers do
341361
opts[:cancel] -> normalize(cancel_issue(issue, opts))
342362
opts[:pr] -> issue_pr(issue, opts)
343363
opts[:project] -> normalize(attach_project(issue, opts[:project]))
364+
opts[:description] -> normalize(update_description(issue, opts[:description]))
344365
opts[:comment] -> :ok
345366
true -> no_action_taken()
346367
end

‎app/lib/linear_cli/linear.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule LinearCli.Linear do
3737
define :attach_issue_to_project, action: :attach_to_project, args: [:project_id]
3838
define :close_issue, action: :close, args: [:state_id]
3939
define :set_issue_status, action: :set_status, args: [:state_id]
40+
define :update_issue_description, action: :update_description, args: [:description]
4041
end
4142

4243
resource LinearCli.Linear.Label do

‎app/lib/linear_cli/linear/issue.ex‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ defmodule LinearCli.Linear.Issue do
5353
argument :state_id, :string, allow_nil?: false
5454
manual LinearCli.Linear.Issue.Update.SetStatus
5555
end
56+
57+
update :update_description do
58+
argument :description, :string, allow_nil?: false
59+
manual LinearCli.Linear.Issue.Update.UpdateDescription
60+
end
5661
end
5762

5863
attributes do
@@ -378,3 +383,16 @@ defmodule LinearCli.Linear.Issue.Update.SetStatus do
378383
})
379384
end
380385
end
386+
387+
defmodule LinearCli.Linear.Issue.Update.UpdateDescription do
388+
@moduledoc false
389+
use Ash.Resource.ManualUpdate
390+
391+
alias LinearCli.Linear.Issue
392+
393+
def update(changeset, _opts, _context) do
394+
Issue.Update.run(changeset.data.identifier, %{
395+
"description" => changeset.arguments.description
396+
})
397+
end
398+
end

‎app/test/linear_cli/cli/issue_commands_test.exs‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,64 @@ defmodule LinearCli.CLI.IssueCommandsTest do
970970
assert output =~ "CRY-1 was closed"
971971
end
972972

973+
test "--description updates the issue description via the issueUpdate mutation" do
974+
test_pid = self()
975+
976+
Req.Test.stub(LinearCli.Api, fn conn ->
977+
{:ok, body, conn} = Plug.Conn.read_body(conn)
978+
decoded = Jason.decode!(body)
979+
query = decoded["query"]
980+
981+
cond do
982+
String.contains?(query, "issue(id: $id)") ->
983+
Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}})
984+
985+
String.contains?(query, "issueUpdate") ->
986+
send(test_pid, {:description, decoded["variables"]["input"]["description"]})
987+
Req.Test.json(conn, issue_updated(%{"description" => "Updated body"}))
988+
989+
true ->
990+
raise "no stub matched query: #{query}"
991+
end
992+
end)
993+
994+
output =
995+
capture_io(fn ->
996+
assert :ok =
997+
LinearCli.CLI.main([
998+
"issue",
999+
"update",
1000+
"--description",
1001+
"Updated body",
1002+
"CRY-1"
1003+
])
1004+
end)
1005+
1006+
assert_received {:description, "Updated body"}
1007+
assert output =~ "CRY-1 description updated"
1008+
end
1009+
1010+
test "-d short flag also updates the issue description" do
1011+
stub_responses([
1012+
{"issue(id: $id)", %{"data" => %{"issue" => issue_map()}}},
1013+
{"issueUpdate", issue_updated(%{"description" => "Short flag body"})}
1014+
])
1015+
1016+
output =
1017+
capture_io(fn ->
1018+
assert :ok =
1019+
LinearCli.CLI.main([
1020+
"issue",
1021+
"update",
1022+
"-d",
1023+
"Short flag body",
1024+
"CRY-1"
1025+
])
1026+
end)
1027+
1028+
assert output =~ "CRY-1 description updated"
1029+
end
1030+
9731031
test "with no issue ids, exits 22 (Ruby: raise SmellsBad -> exit 22)" do
9741032
test_pid = self()
9751033
halt = fn code -> send(test_pid, {:halted, code}) end

‎app/test/linear_cli/cli/issue_helpers_test.exs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,26 @@ defmodule LinearCli.CLI.IssueHelpersTest do
263263
end
264264
end
265265

266+
describe "update_description/2" do
267+
test "resolves and sends the description, printing a confirmation" do
268+
stub_responses([{"issueUpdate", issue_updated(%{"description" => "New body"})}])
269+
270+
assert capture_io(fn ->
271+
assert {:ok, %Issue{description: "New body"}} =
272+
IssueHelpers.update_description(issue(), "New body")
273+
end) =~ "CRY-1 description updated"
274+
end
275+
276+
test "propagates an API error without printing confirmation" do
277+
stub_responses([{"issueUpdate", %{"errors" => [%{"message" => "boom"}]}}])
278+
279+
assert capture_io(fn ->
280+
assert {:error, %Ash.Error.Invalid{}} =
281+
IssueHelpers.update_description(issue(), "New body")
282+
end) == ""
283+
end
284+
end
285+
266286
describe "attach_project/2 (Ruby: CLI::Issue#attach_project)" do
267287
test "resolves the project by name against the team's projects and attaches it" do
268288
stub_responses([
@@ -363,6 +383,17 @@ defmodule LinearCli.CLI.IssueHelpersTest do
363383
assert output =~ "CRY-1 was attached to Manhattan Rollout"
364384
end
365385

386+
test "with :description, updates the issue description" do
387+
stub_responses([{"issueUpdate", issue_updated(%{"description" => "New body"})}])
388+
389+
output =
390+
capture_io(fn ->
391+
assert :ok = IssueHelpers.update_issue(issue(), description: "New body")
392+
end)
393+
394+
assert output =~ "CRY-1 description updated"
395+
end
396+
366397
test "with only :comment, comments and stops without the 'no action taken' warning" do
367398
stub_responses([{"commentCreate", comment_created()}])
368399

‎app/test/linear_cli/linear/issue_test.exs‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,49 @@ defmodule LinearCli.Linear.IssueTest do
376376
assert {:error, %Ash.Error.Invalid{}} = Linear.close_issue(issue, "nope")
377377
end
378378
end
379+
380+
describe "update_issue_description/2" do
381+
test "sends description and returns the issue refetched via full_fields" do
382+
issue = struct!(LinearCli.Linear.Issue, id: "i1", identifier: "CRY-1")
383+
384+
Req.Test.stub(LinearCli.Api, fn conn ->
385+
{:ok, body, conn} = Plug.Conn.read_body(conn)
386+
%{"variables" => %{"id" => id, "input" => input}} = Jason.decode!(body)
387+
388+
assert id == "CRY-1"
389+
assert input == %{"description" => "Updated body"}
390+
391+
Req.Test.json(conn, %{
392+
"data" => %{
393+
"issueUpdate" => %{
394+
"issue" => %{
395+
"id" => "i1",
396+
"identifier" => "CRY-1",
397+
"title" => "Fix it",
398+
"branchName" => "cry-1-fix-it",
399+
"description" => "Updated body",
400+
"assignee" => nil,
401+
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
402+
"comments" => %{"nodes" => []}
403+
}
404+
}
405+
}
406+
})
407+
end)
408+
409+
assert {:ok, updated} = Linear.update_issue_description(issue, "Updated body")
410+
assert updated.description == "Updated body"
411+
end
412+
413+
test "surfaces a GraphQL error" do
414+
issue = struct!(LinearCli.Linear.Issue, id: "i1", identifier: "CRY-1")
415+
416+
Req.Test.stub(LinearCli.Api, fn conn ->
417+
Req.Test.json(conn, %{"errors" => [%{"message" => "unauthorized"}]})
418+
end)
419+
420+
assert {:error, %Ash.Error.Invalid{}} =
421+
Linear.update_issue_description(issue, "Updated body")
422+
end
423+
end
379424
end

0 commit comments

Comments
 (0)