Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ $ lcomment CRY-1234 CRY-3 <5>
$ lc issue update --close --reason "These were closable" CRY-1234 CRY-2
----

==== Post a project status update

Not in Ruby's `linear-cli` - a status post on a project (Linear's own "Project
Update" feature), not an edit to the project itself.

[source,sh]
----
$ lc project update Manhattan --body "Shipping ahead of schedule" --health onTrack
----

=== Wrapper scripts

The `bin/` wrapper scripts (ported verbatim from `linear-cli`'s own `exe/scripts/`)
Expand Down
26 changes: 26 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ defmodule LinearCli.CLI do
defp dispatch([:version], result, halt), do: run(&Commands.version/1, result, halt)
defp dispatch([:team, :list], result, halt), do: run(&Commands.team_list/1, result, halt)
defp dispatch([:project, :list], result, halt), do: run(&Commands.project_list/1, result, halt)

defp dispatch([:project, :update], result, halt),
do: run(&Commands.project_update/1, result, halt)

defp dispatch([:issue, :list], result, halt), do: run(&Commands.issue_list/1, result, halt)
defp dispatch([:issue, :create], result, halt), do: run(&Commands.issue_create/1, result, halt)

Expand Down Expand Up @@ -345,6 +349,28 @@ defmodule LinearCli.CLI do
options: [
team: [short: "-t", long: "--team", help: "Show projects for only this team"]
]
],
update: [
name: "update",
about: "Post a status update to a project",
args: [
project: [
value_name: "PROJECT",
help: "Project name, URL, ID, or search term",
required: true
]
Comment on lines +357 to +361
],
options: [
body: [short: "-b", long: "--body", help: "The update's content (markdown)"],
health: [
long: "--health",
help: "Project health: onTrack, atRisk, or offTrack",
parser: fn
v when v in ["onTrack", "atRisk", "offTrack"] -> {:ok, v}
v -> {:error, "must be one of: onTrack, atRisk, offTrack (got #{inspect(v)})"}
end
]
]
]
]
],
Expand Down
20 changes: 20 additions & 0 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ defmodule LinearCli.CLI.Commands do
defp projects_for(%{mine: true}, _options), do: Linear.my_projects()
defp projects_for(_flags, _options), do: Linear.projects()

@doc """
New in this port - Ruby has no equivalent. Posts a status update
(Linear's own "Project Update" feature - a journal-style status post,
not an edit to the project's own fields) via the projectUpdateCreate
mutation. `PROJECT` is resolved the same way issue list's `--project`
is - against every project in the workspace, prompting if ambiguous.
"""
def project_update(%{args: %{project: search}, options: options}) do
with {:ok, projects} <- Linear.projects(),
project when not is_nil(project) <- Projects.project_for(projects, search),
{:ok, update} <-
Linear.post_project_update(project.id, options.body, %{health: options.health}) do
Display.show(update, %{output: options.output})
:ok
else
nil -> {:error, {:smells_bad, "No project found matching #{search}"}}
{:error, reason} -> {:error, reason}
end
end
Comment on lines +69 to +80

@doc """
Ported from commands/issue/list.rb + operations/issue/list.rb.

Expand Down
7 changes: 6 additions & 1 deletion app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule LinearCli.CLI.Display do
own `#to_s`/`#full`/`#display` methods.
"""

alias LinearCli.Linear.{Issue, Project, Team, User}
alias LinearCli.Linear.{Issue, Project, ProjectUpdate, Team, User}

@ash_internal_fields ~w(__meta__ __metadata__ __order__ __lateral_join_source__ aggregates calculations)a

Expand All @@ -31,6 +31,11 @@ defmodule LinearCli.CLI.Display do
IO.puts("#{String.pad_trailing(project.name || "", 12)} #{project.url}")
end

defp puts_text(%ProjectUpdate{} = update, _opts) do
health = if update.health, do: " (#{update.health})", else: ""
IO.puts("Posted#{health}: #{update.url}")
end

defp puts_text(%User{} = user, opts) do
IO.puts(user_line(user, opts))
end
Expand Down
4 changes: 4 additions & 0 deletions app/lib/linear_cli/linear.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,9 @@ defmodule LinearCli.Linear do
resource LinearCli.Linear.Comment do
define :add_comment, action: :create, args: [:issue_identifier, :body]
end

resource LinearCli.Linear.ProjectUpdate do
define :post_project_update, action: :create, args: [:project_id, :body]
end
end
end
79 changes: 79 additions & 0 deletions app/lib/linear_cli/linear/project_update.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
defmodule LinearCli.Linear.ProjectUpdate do
@moduledoc """
A Linear project update - a status post on a project (e.g. "This week's
progress..."), distinct from editing the project's own fields. New in
this port - Ruby has no equivalent. Created via the projectUpdateCreate
mutation (schema/LinearAPI.graphql).
"""

use Ash.Resource, domain: LinearCli.Linear

actions do
create :create do
argument :project_id, :string, allow_nil?: false
argument :body, :string, allow_nil?: false
argument :health, :string, allow_nil?: true
manual LinearCli.Linear.ProjectUpdate.Create
end
end

attributes do
attribute :id, :string, primary_key?: true, allow_nil?: false, public?: true
attribute :body, :string, public?: true
attribute :health, :string, public?: true
attribute :url, :string, public?: true
end

@doc "GraphQL field selection for a project update's own fields."
def base_fields do
"id body health url createdAt"
end

@doc false
def from_map(map) do
struct!(__MODULE__,
id: map["id"],
body: map["body"],
health: map["health"],
url: map["url"]
)
end
end

defmodule LinearCli.Linear.ProjectUpdate.Create do
@moduledoc false
use Ash.Resource.ManualCreate

alias LinearCli.Api
alias LinearCli.Linear.ProjectUpdate

def create(changeset, _opts, _context) do
args = changeset.arguments

input =
%{"projectId" => args.project_id, "body" => args.body}
|> maybe_put_health(args.health)

case Api.call(document(), %{"input" => input}) do
{:ok, %{"projectUpdateCreate" => %{"projectUpdate" => update_map}}}
when is_map(update_map) ->
{:ok, ProjectUpdate.from_map(update_map)}

{:ok, other} ->
{:error, {:unexpected_response, other}}

{:error, reason} ->
{:error, reason}
end
end

defp maybe_put_health(input, nil), do: input
defp maybe_put_health(input, health), do: Map.put(input, "health", health)

# A function, not a module attribute: ProjectUpdate.base_fields/0 reaches
# into no other file today, but kept consistent with every other
# document/0 in this codebase for the same reason they all are.
defp document do
"mutation($input: ProjectUpdateCreateInput!) { projectUpdateCreate(input: $input) { projectUpdate { #{ProjectUpdate.base_fields()} } } }"
end
end
73 changes: 72 additions & 1 deletion app/test/linear_cli/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,77 @@ defmodule LinearCli.CLITest do
assert capture_io(fn -> LinearCli.CLI.main(["project", "list"]) end) =~ "Manhattan"
end

test "project update resolves the project by name and posts a status update" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
query = decoded["query"]

cond do
String.contains?(query, "projects(first: $first") ->
Req.Test.json(conn, %{
"data" => %{
"projects" => %{
"edges" => [
%{
"node" => %{
"id" => "p1",
"name" => "Manhattan",
"slugId" => "abc",
"url" => "https://linear.app/x/project/manhattan-abc"
},
"cursor" => "p1"
}
],
"pageInfo" => %{"hasNextPage" => false}
}
}
})

String.contains?(query, "projectUpdateCreate") ->
send(test_pid, {:input, decoded["variables"]["input"]})

Req.Test.json(conn, %{
"data" => %{
"projectUpdateCreate" => %{
"projectUpdate" => %{
"id" => "pu1",
"body" => "Doing great",
"health" => "onTrack",
"url" => "https://linear.app/x/update/pu1"
}
}
}
})

true ->
raise "no stub matched query: #{query}"
end
end)

output =
capture_io(fn ->
assert :ok =
LinearCli.CLI.main([
"project",
"update",
"Manhattan",
"--body",
"Doing great",
"--health",
"onTrack"
])
end)

assert output =~ "onTrack"
assert output =~ "https://linear.app/x/update/pu1"

assert_received {:input,
%{"projectId" => "p1", "body" => "Doing great", "health" => "onTrack"}}
end

test "issue list prints a one-line summary per issue" do
assert capture_io(fn -> LinearCli.CLI.main(["issue", "list"]) end) =~ "CRY-1"
end
Expand Down Expand Up @@ -281,7 +352,7 @@ defmodule LinearCli.CLITest do

assert_received {:halted, 1}
assert output =~ "Manage projects"
assert output =~ "list List projects"
assert output =~ "List projects"
end

test "an unexpected raise (not a returned error) still degrades to exit 88, not a raw crash" do
Expand Down